很多时候我们需要用mybatis来做数据库orm,基于mybatis 优秀的基因,我们能够轻松的搞定数据库的orm。
但是mybatis一般的使用方法都是一个同步查询,主线程调用数据库查询操作,等待返回结果,这样在高并发网络情况下代价是很高的。所以我们需要封装一套提供异步查询回调机制。
异步操作。提到异步操作,我们就得提到回调接口。回调接口就是通过在主线程监听其他线程执行完的结果取得返回值。或者做一些逻辑处理,常用的接口在jdk提供了Future,Callable等机制,不过一个是阻塞,一个异步,所以我们常用的是Callable机制。
下面我们来手动写这么一套封装框架,实现异步操作。
首先我们想要的是这种机制:
mapper.method(parm,parm,new 回调函数(){
success(data){}
error(erroe){}
}
}));
我们能够在回调函数里面获取到执行结果。所以我们要将结果写入到回调函数里面,基于mybatis 的mapper机制。sql语句利用xml表示,通过id于方法名称对应起来。所以我们想要异步的话就必须获取到这个mapper的代理类。通过代理类构造一个代理对象,传入方法名称,参数,然后执行代理方法。然后将代理任务提交给mybatis去执行sql语句,最后将结果返回给回调函数。
所以我们有了大体思路。我们通过大概的流程图来描绘一下。
首先我们需要一个执行数据库操作的线程,我们通过线程池的模式来管理线程。要是对线程池不熟悉的小伙伴,还希望自行补脑。
如下代码所示:
/**
* 异步mapper 提交任务执行器
*
* @author twjitm - [Created on 2018-09-11 11:02]
* @company http://www.twjitm.com/
* @jdk java version "1.8.0_77"
*/
public class AsyncMapperExecutor {
private static int MAX_QUEUE_SIZE;
private static int CORE_POOL_SIZE;
private static int MAX_POOL_SIZE;
private static String THREAD_NAME;
private static volatile ThreadPoolExecutor executorService = null;
/**
* 初始化异步db执行线程
*
* @param corePoolSize
* @param maxPoolSize
* @param queueSize
*/
public static void init(int corePoolSize, int maxPoolSize, int queueSize,String threadName) {
CORE_POOL_SIZE = corePoolSize;
MAX_POOL_SIZE = maxPoolSize;
MAX_QUEUE_SIZE = queueSize;
THREAD_NAME=threadName;
init();
}
public static void init() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(MAX_QUEUE_SIZE), new ThreadFactory() {
private AtomicInteger counter = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName(THREAD_NAME + counter.getAndIncrement());
t.setDaemon(false);
return t;
}
});
}
}
/**
* 异步执行一个任务
*
* @param mapper
* @param method
* @param args
* @param callback
*/
public static void executeRunnable(Object mapper, Method method, Object[] args, TaskBackHandler callback) {
checkNull();
AsyncMapperTask executor = new AsyncMapperTask(mapper, method, args, callback);
executorService.execute(executor);
}
private static void checkNull() {
if (executorService == null) {
throw new AsyncMapperException("数据库异步执行线程需要初始化");
}
}
}
此类有两个功能,一个是负责初始化线程池,另一个就是提供执行线程池操作。通过提交的代理参数,初始化一个异步任务,最后提交。
接下来我们需要一个生产mapper的工厂,其实就是对mybats提供的接口进行封装。
/**
* 不要轻易去生产一个{@link Connection}对象。
* <p>
* sql session 工厂
*
* @author twjitm - [Created on 2018-09-10 18:40]
* @company http://www.twjitm.com/
* @jdk java version "1.8.0_77"
*/
public class NettySqlSessionFactory {
private static SqlSessionFactory SQL_SESSION_FACTORY;
public static void init(String resource){
// String resource = "db/mybatis-config.xml";
Reader reader = null;
try {
reader = Resources.getResourceAsReader(resource);
} catch (IOException e) {
System.out.println(e.getMessage());
}
SQL_SESSION_FACTORY = new SqlSessionFactoryBuilder().build(reader);
}
/**
* 具体请查看mybatis 的config
*
* @return
*/
public static SqlSession getSqlSession() {
return SQL_SESSION_FACTORY.openSession(true);
}
有了它,我们就能够连接上数据库,因此我们还缺一个动态代理对象,用来代理我们自定义的mapper接口类。
/**
* @author twjitm - [Created on 2018-09-11 10:52]
* @company http://www.twjitm.com/
* @jdk java version "1.8.0_77"
*/
public class AsyncMapperObjectProxy<T> implements InvocationHandler {
private T mapper;
public AsyncMapperObjectProxy(T mapper) {
this.mapper = mapper;
}
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
if (isCallbackMethod(method, params)) {
TaskBackHandler<T> handler = null;
for (Object param : params) {
if (param != null) {
if (TaskBackHandler.class.isAssignableFrom(param.getClass())) {
handler = (TaskBackHandler<T>) param;
}
}
}
AsyncMapperExecutor.executeRunnable(mapper, method, params, handler);
return null;
} else {
return method.invoke(mapper, params);
}
}
/**
* 是否为异步调用mapper方法
*
* @param method
* @param args
* @return
*/
private boolean isCallbackMethod(Method method, Object[] args) {
boolean hasAsyncCallback = false;
if (args == null) {
return false;
}
int count = 0;
for (Object arg : args) {
if (arg != null) {
if (TaskBackHandler.class.isAssignableFrom(arg.getClass())) {
hasAsyncCallback = true;
count++;
}
}
}
if (hasAsyncCallback && count > 1) {
throw new AsyncMapperException(
"Method[" + method.getName() + "] has more than one callback" +
" method!");
}
return hasAsyncCallback;
}
}
有了它,我们能够代理得到一个mapper对象了,因此我们为了代理简洁,我们需要一个mapper代理工厂,工厂主要是获取一个mapper对象。代码很简单,如下所示。
/**
* mapper bean 工厂
*
* @author twjitm - [Created on 2018-09-11 11:26]
* @company http://www.twjitm.com/
* @jdk java version "1.8.0_77"
*/
public class MapperBeanFactory {
/**
* 通过代理,获取一个mapper对象
*
* @return
* @throws Exception
*/
public static Object getMapperBean(Class mapperInterface) throws Exception {
Object mapper = NettySqlSessionFactory.getSqlSession().getMapper(mapperInterface);
AsyncMapperObjectProxy asyncMapperObjectProxy = new AsyncMapperObjectProxy<>(mapper);
return Proxy.newProxyInstance(mapperInterface.getClassLoader(),
new Class[]{mapperInterface}, asyncMapperObjectProxy);
}
}
最后我们需要一个异步任务对象
/**
* @author twjitm - [Created on 2018-09-11 11:16]
* @company http://www.twjitm.com/
* @jdk java version "1.8.0_77"
*/
public class AsyncMapperTask<T> implements Runnable {
Logger logger = LoggerFactory.getLogger(AsyncMapperTask.class);
private TaskBackHandler<T> callback;
private Object mapper;
private Method method;
private Object[] args;
public AsyncMapperTask(Object mapper, Method method, Object[] args, TaskBackHandler<T> callback) {
this.mapper = mapper;
this.method = method;
this.args = args;
this.callback = callback;
}
@Override
@SuppressWarnings("unchecked")
public void run() {
T result;
try {
result = (T) method.invoke(mapper, args);
callback.taskBack( true,result);
} catch (Exception e) {
// 仅仅捕获数据库访问异常
logger.error("执行数据库查询发生异常", e);
try {
callback.taskBack(false, null);
} catch (Exception e1) {
e1.printStackTrace();
logger.error("执行数据库查询发生异常", e1);
}
}
}
}
有了以上代码的功能,我们能够异步查询了,因此我们来测试一下。首先我们添加mybatis的基本配置文件。
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--加载配置文件-->
<properties resource="db/db_jdbc.properties"/>
<settings>
<setting name="cacheEnabled" value="true"/>
<!-- 打开延迟加载的开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!--开启打印日志-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<!--允许嵌套语句中是否使用分页-->
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<!--一级缓存-->
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<!--懒加载方法-->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
<typeAliases>
<!--
type:类型路径
alias:别名
<typeAlias type="cn.twjitm.mybatis.po.User" alias="user"/>
-->
<!-- 批量别名定义
指定包名:mybatis自动扫描包中的po类,自动定义别名
别名就是类名(首字母大小写都可以)
-->
<typeAlias alias="User" type="com.twjtim.server.core.mybatis.User"/>
</typeAliases>
<environments default="development">
<environment id="development">
<!--使用jdbc事务机制-->
<transactionManager type="jdbc"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/db_0?characterEncoding=utf8&useSSL=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<!--连接池配置-->
<property name="poolMaximumIdleConnections" value="0"/>
<property name="poolMaximumActiveConnections" value="10000"/>
<property name="poolPingQuery" value="SELECT 1 FROM DUAL"/>
<property name="poolPingEnabled" value="true"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- 加载单个mapper -->
<mapper resource="db/mapper/UserMapper.xml"/>
</mappers>
</configuration>
然后我们在数据库创建一张user表;
表结构很简单,足以说明问题。
然后我们通过逆向生成代码也行,手写mapper.xml文件也想,我相信若是看到这篇文章的人应该都会逆向生成的吧,
mybatis+generator[传送门:]
我们得到User这样的一个类和一个Usermapper.xml和UserMapper.java这样的一个文件,放到正确的位置。然后我们就改写
UserMapper.java 这个类了
我们想要一部操作,首先我们要给每个方法注入一个异步接口。 异步接口通过封装,一个方法足够调用了。
/**
* 异步 mybatis 的mapper方法执行成功或失败的回调函数
*
* @author twjitm - [Created on 2018-09-11 10:52]
* @company http://www.twjitm.com/
* @jdk java version "1.8.0_77"
*/
public interface TaskBackHandler<T> {
/**
* 执行成功
*
* @param result
*/
void taskBack(final boolean successful,T result) throws Exception;
}
在UserMapper.java 这个类中我们对方法添加上异步接口,如下代码
public interface UserMapper {
void getUser(String name,TaskBackHandler<List<User>> callback);
void insertUser(User user,TaskBackHandler<User> callback);
void getUserByName(String name,Integer age,TaskBackHandler<User> callback);
}
最后我们写一个测试类来测试一下
public class TestMybatis {
public static void main(String[] args) {
DbServiceConfig.init("db/mybatis-config.xml");
//异步回调接口
try {
UserMapper userMapper = (UserMapper) MapperBeanFactory.getMapperBean(UserMapper.class);
userMapper.getUserByName("tangwenjiang",23,((successful, result) -> {
if(result!=null){
System.out.println( result.getName());
}
}));
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("主线程后面的逻辑");
}
}
日志