手写MyBatis2.0附带Plugin功能(增强版本)
基于上一篇博客,手写MyBatis1.0末尾提出的几个不足之处与需要新增的地方,这篇博客将完善之前的MyBatis1.0版本,升级为2.0版本~将会新增的功能:
- 加入Plugin插件功能。
- 加入缓存功能。
- 分解Executor指责,分出各个类使其符合单一职责原则。
- 使用注解灵活配置mapper与实体。
代码中注释打的很清楚了,文字只简单描述一下。
源码链接(包括v1.0与v2.0): https://github.com/staticLin/customMyBatis.git
首先是SqlSession的改动,只改了构造器,将持有的executor变为动态新增的。
/**
* 用构造器将两个对象形成关系
*/
public CustomSqlSession(CustomConfiguration configuration) {
this.configuration = configuration;
//这里需要决定是否开启缓存,则从Configuraton中判断是否需要缓存,创建对应Executor
this.executor = configuration.newExecutor();
}
为了客户端方便调用,做了一个SqlSessionFactory负责接收信息初始化Configuration与创建SqlSession
/**
* @description: 创建SqlSession工厂
* @author: linyh
* @create: 2018-11-01 17:49
**/
public class SqlSessionFactory {
private CustomConfiguration configuration;
/**
* 以下build方法将初始化Factory的属性Configuration,具体工作在Configuration构造器中完成
*/
public SqlSessionFactory build(String mapperPath) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
return this.build(mapperPath, null, false);
}
public SqlSessionFactory build(String mapperPath, String[] pluginPath) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
return this.build(mapperPath, pluginPath, false);
}
public SqlSessionFactory build(String mapperPath, String[] pluginPath, boolean enableCache) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
configuration = new CustomConfiguration(mapperPath, pluginPath, enableCache);
return this;
}
/**
* 根据配置信息(Configuration)获取对应的SqlSession
*/
public CustomSqlSession openSqlSession(){
return new CustomSqlSession(configuration);
}
}
然后是Configuration,值得一提的是新增扫描包**解获取运行时信息功能,新增了插件功能,这里只做了对executor的拦截,所以新增一个创建executor方法,实现动态选择executor。
/**
* @description:
* @author: linyh
* @create: 2018-10-31 16:32
**/
public class CustomConfiguration {
public static final MapperRegistory mapperRegistory = new MapperRegistory();
public static final Map<String, String> mappedStatements = new HashMap<>();
private CustomInterceptorChain interceptorChain = new CustomInterceptorChain();
private boolean enableCache = false;
private List<Class<?>> mapperList = new ArrayList<>();
private List<String> classPaths = new ArrayList<>();
/**
* 初始化时Configuration加载所有Mapper信息、plugin信息、缓存是否开启信息
*/
public CustomConfiguration(String mapperPath, String[] pluginPath, boolean enableCache) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
//扫描mapper路径,将必要的mapper信息存入mapperRegistory与mapperStatements
scanPackage(mapperPath);
for (Class<?> mapper : mapperList) {
//当类为接口时视其为mapper,开始解析它
//Myabtis中判断是否为mapper还用到了isIndependent的方法判断,较为复杂,这里简化,体现思想即可
if (mapper.isInterface()) {
parsingClass(mapper);
}
}
if (pluginPath != null) {
//遍历plugin路径,初始化plugin并放入list中
for (String plugin : pluginPath) {
Interceptor interceptor = (Interceptor) Class.forName(plugin).newInstance();
interceptorChain.addInterceptor(interceptor);
}
}
//设置缓存是否开启
this.enableCache = enableCache;
}
/**
* MapperProxy根据statementName查找是否有对应SQL
*/
public boolean hasStatement(String statementName) {
return mappedStatements.containsKey(statementName);
}
/**
* MapperProxy根据statementID获取SQL
*/
public String getMappedStatement(String id) {
return mappedStatements.get(id);
}
public <T> T getMapper(Class<T> clazz, CustomSqlSession sqlSession) {
return mapperRegistory.getMapper(clazz, sqlSession);
}
/**
* 创建一个Executor(因为加入了plugin功能,需要判断是否创建带plugin的executor)
*/
public CustomExecutor newExecutor() {
CustomExecutor executor = createExecutor();
if (interceptorChain.hasPlugin()) {
return (CustomExecutor)interceptorChain.pluginAll(executor);
}
return executor;
}
/**
* 创建一个Executor(需要判断是否创建带缓存功能的executor)
*/
private CustomExecutor createExecutor(){
if (enableCache) {
return new CacheExecutor(new SimpleExecutor());
}
return new SimpleExecutor();
}
/**
* 解析类中的注解
*/
private void parsingClass(Class<?> mapper) {
//如有Pojo注解,则可以获取到实体类的信息
if (mapper.isAnnotationPresent(Pojo.class)) {
for (Annotation annotation : mapper.getAnnotations()) {
if (annotation.annotationType().equals(Pojo.class)) {
//将mapper与实体类信息注册进mapperRegistory中
mapperRegistory.addMapper(mapper, ((Pojo)annotation).value());
}
}
}
Method[] methods = mapper.getMethods();
for (Method method : methods) {
//TODO 新增Update、Delete、Insert注解
//如果有Select注解就解析SQL语句
if (method.isAnnotationPresent(Select.class)) {
for (Annotation annotation : method.getDeclaredAnnotations()) {
if (annotation.annotationType().equals(Select.class)) {
//将方法名与SQL语句注册进mappedStatements中
mappedStatements.put(method.getDeclaringClass().getName() + "." +method.getName(),
((Select) annotation).value());
}
}
}
}
}
/**
* 扫描包名,获取包下的所有.class文件
*/
private void scanPackage(String mapperPath) throws ClassNotFoundException {
String classPath = TestMybatis.class.getResource("/").getPath();
mapperPath = mapperPath.replace(".", File.separator);
String mainPath = classPath + mapperPath;
doPath(new File(mainPath));
for (String className : classPaths) {
className = className.replace(classPath.replace("/","\\").replaceFirst("\\\\",""),"").replace("\\",".").replace(".class","");
Class<?> clazz = Class.forName(className);
mapperList.add(clazz);
}
}
/**
* 该方法会得到所有的类,将类的绝对路径写入到classPaths中
*/
private void doPath(File file) {
if (file.isDirectory()) {//文件夹
//文件夹我们就递归
File[] files = file.listFiles();
for (File f1 : files) {
doPath(f1);
}
} else {//标准文件
//标准文件我们就判断是否是class文件
if (file.getName().endsWith(".class")) {
//如果是class文件我们就放入我们的集合中。
classPaths.add(file.getPath());
}
}
}
}
然后是三个简单的注解创建。
/**
* @Author:linyh
* @Date: 2018/11/2 10:38
* @Modified By:
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Method {
String value();
}
/**
* @Author:linyh
* @Date: 2018/11/1 17:40
* @Modified By:
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Pojo {
Class<?> value();
}
/**
* @Author:linyh
* @Date: 2018/11/1 17:37
* @Modified By:
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
String value();
}
在mapper中这样使用,为了解析到SQL语句与mapper对应的实体类。
/**
* @Author:linyh
* @Date: 2018/10/31 16:56
* @Modified By:
*/
@Pojo(Test.class)
public interface TestCustomMapper {
@Select("select * from test where id = %d")
Test selectByPrimaryKey(int id);
}
然后是之前的默认executor,细化了职责。
/**
* @description: 自定义Executor
* @author: linyh
* @create: 2018-10-31 17:46
**/
public class SimpleExecutor implements CustomExecutor {
/**
* 这里为默认Executor,细化了责任,查询交给StatementHandler处理
*/
@Override
public <T> T query(String statement, String parameter, Class pojo) throws IllegalAccessException, SQLException, InstantiationException, InvocationTargetException, NoSuchMethodException {
StatementHandler statementHandler = new StatementHandler();
return statementHandler.query(statement, parameter, pojo);
}
}
然后是使用了装饰器模式的在默认executor的功能之上新增缓存功能的executor
/**
* @description: 缓存的Executor,用了装饰器模式
* @author: linyh
* @create: 2018-11-01 18:02
**/
public class CacheExecutor implements CustomExecutor{
//这里的delegate为被装饰的executor
private CustomExecutor delegate;
private static final Map<Integer, Object> cache = new HashMap<>();
/**
* 装饰executor,为了在原有的executor之上增加新的功能,扩展为意图
*/
public CacheExecutor(CustomExecutor delegate) {
this.delegate = delegate;
}
/**
* 修改了查询方法,扩展了缓存功能。
* 由于只实现了查询功能,所以这里的缓存处理没有清空的处理。
* 后续升级版本时需要新增缓存更新方法。
*/
@Override
public <T> T query(String statement, String parameter, Class pojo) throws IllegalAccessException, SQLException, InstantiationException, InvocationTargetException, NoSuchMethodException {
//这里的CacheKey为缓存的关键
CacheKey cacheKey = new CacheKey();
//根据SQL语句与参数两个维度来判断每次查询是否相同
//底层原理为hashCode的计算,同一个SQL语句与同一个参数将视为同一个查询,cacheKey中的code也会一样
//在Mybatis中的cacheKey里的code维度有更多,这里简化体现思想即可
cacheKey.update(statement);
cacheKey.update(parameter);
//判断Map中是否含有根据SQL语句与参数算出来的code
if (!cache.containsKey(cacheKey.getCode())) {
//如没有就用被装饰者的查询方法,然后新增缓存
Object obj = delegate.query(statement, parameter, pojo);
cache.put(cacheKey.getCode(), obj);
return (T)obj;
}
//缓存命中
System.out.println("从缓存拿数据" + cache.get(cacheKey.getCode()));
return (T)cache.get(cacheKey.getCode());
}
}
顺便贴上缓存核心实现类(核心思想是把一次查询分为几个维度去计算hashCode,相同SQL语句与相同参数的每次查询算出来的Code将是相同的,即可视为同一查询)
/**
* @description: 缓存key值
* @author: linyh
* @create: 2018-11-02 11:22
**/
public class CacheKey {
//沿用MyBatis设计的默认值
private static final int DEFAULT_HASHCODE = 17;
private static final int DEFAULT_MULTIPLYER = 37;
private int hashCode;
private int count;
private int multiplyer;
public CacheKey() {
this.hashCode = DEFAULT_HASHCODE;
this.count = 0;
this.multiplyer = DEFAULT_MULTIPLYER;
}
/**
* 计算每一个传进来的Object的独有的一个code
*/
public void update(Object object){
if (object != null && object.getClass().isArray()) {
int length = Array.getLength(object);
for (int i = 0; i < length; i++) {
Object element = Array.get(object, i);
doUpdate(element);
}
}else {
doUpdate(object);
}
}
public int getCode() {
return hashCode;
}
private void doUpdate(Object o) {
int baseHashCode = o == null ? 1 : o.hashCode();
count++;
baseHashCode *= count;
hashCode = multiplyer * hashCode + baseHashCode;
}
}
然后是负责查询工作的类
/**
* @description: 负责查询工作
* @author: linyh
* @create: 2018-11-02 10:10
**/
public class StatementHandler {
private ResultSetHandler resultSetHandler = new ResultSetHandler();
/**
* 主要执行查询工作的地方
*/
public <T> T query(String statement, String parameter, Class pojo) throws SQLException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Connection conn = null;
//TODO 这里需要一个ParameterHandler,然后用TypeHandler设置参数,偷懒没写TypeHandler...
statement = String.format(statement, Integer.parseInt(parameter));
try {
conn = getConnection();
PreparedStatement preparedStatement = conn.prepareStatement(statement);
preparedStatement.execute();
//查询到这里结束,映射结果的工作委派给ResultSetHandler去做
return resultSetHandler.handle(preparedStatement.getResultSet(), pojo);
} finally {
if (conn != null) {
conn.close();
conn = null;
}
}
}
private Connection getConnection() throws SQLException {
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://127.0.0.1:3306/gp?serverTimezone=UTC";
String username = "root";
String password = "admin";
Connection conn = null;
try {
Class.forName(driver); //classLoader,加载对应驱动
conn = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
}
负责映射结果集的类
/**
* @description: 负责结果的映射
* @author: linyh
* @create: 2018-11-02 10:10
**/
public class ResultSetHandler {
/**
* 细化了executor的职责,专门负责结果映射
*/
public <T> T handle(ResultSet resultSet, Class pojo) throws IllegalAccessException, InstantiationException, NoSuchMethodException, SQLException, InvocationTargetException {
//代替了ObjectFactory
Object pojoObj = pojo.newInstance();
//遍历pojo中每一个属性域去设置参数
if (resultSet.next()) {
for (Field field : pojoObj.getClass().getDeclaredFields()) {
setValue(pojoObj, field, resultSet);
}
}
return (T)pojoObj;
}
/**
* 利用反射给每一个属性设置参数
*/
private void setValue(Object pojo, Field field, ResultSet rs) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, SQLException {
Method setMethod = pojo.getClass().getMethod("set" + firstWordCapital(field.getName()), field.getType());
setMethod.invoke(pojo, getResult(rs, field));
}
/**
* 根据反射判断类型,从ResultSet中取对应类型参数
*/
private Object getResult(ResultSet rs, Field field) throws SQLException {
//TODO 这里需要用TypeHandle处理,偷懒简化了,后续升级点
Class type = field.getType();
if (Integer.class == type) {
return rs.getInt(field.getName());
}
if (String.class == type) {
return rs.getString(field.getName());
}
return rs.getString(field.getName());
}
/**
* 将一个单词首字母大写
*/
private String firstWordCapital(String word){
String first = word.substring(0, 1);
String tail = word.substring(1);
return first.toUpperCase() + tail;
}
}
其他的MapperProxy之类的没有什么改动,只是新增了一个实体类的属性,为了传递给结果集映射时使用。
最后介绍Plugin的实现~首先是保存所有plugin的类(此类将作为Configuration属性存放)。
/**
* @description: 插件链存放类
* @author: linyh
* @create: 2018-11-01 17:59
**/
public class CustomInterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public void addInterceptor(Interceptor interceptor){
interceptors.add(interceptor);
}
/**
* 将目标executor依此按照插件链动态代理target
* 如有多个plugin,多次代理(如b插件代理了a执行器,c插件又代理a执行器,此时执行顺序为c -> b -> a)
*/
public Object pluginAll(Object target){
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public boolean hasPlugin(){
if (interceptors.size() == 0) {
return false;
}
return true;
}
}
插件的接口
/**
* @description:
* @author: linyh
* @create: 2018-11-02 10:12
**/
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
}
自定义插件实现上面的接口
/**
* @description: 测试用插件
* @author: linyh
* @create: 2018-11-02 10:29
**/
@Method("query")
public class testPlugin implements Interceptor{
@Override
public Object intercept(Invocation invocation) throws Throwable {
String statement = (String) invocation.getArgs()[0];
String parameter = (String) invocation.getArgs()[1];
Class pojo = (Class) invocation.getArgs()[2];
System.out.println("----------plugin生效----------");
System.out.println("executor执行query方法前拦截,拦截到的Sql语句为: " + statement);
System.out.println("参数为: " + parameter + " 实体类为: " + pojo.getName());
System.out.println("-----------拦截结束-----------");
//在这里执行原executor的方法
return invocation.proceed();
}
/**
* 实际上此方法就是将此插件给target做一个动态代理
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
一个执行原方法的类。
/**
* @description: 此类为辅助plugin执行原方法的类,存放原executor、原方法、原参数
* @author: linyh
* @create: 2018-11-02 10:13
**/
public class Invocation {
private Object target;
private Method method;
private Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
/**
* 在这里执行原executor的方法
*/
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}
然后是一个核心实现类,插件实际的代理者。
/**
* @description: 插件代理者
* @author: linyh
* @create: 2018-11-02 10:30
**/
public class Plugin implements InvocationHandler {
private Object target;
private Interceptor interceptor;
/**
* @param target 被代理的Executor类
* @param interceptor plugin插件
*/
public Plugin(Object target, Interceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
}
/**
* 包装executor的方法
* @param obj 被代理的Executor类
* @param interceptor plugin插件
* @return 代理类
*/
public static Object wrap(Object obj, Interceptor interceptor) {
Class clazz = obj.getClass();
return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), new Plugin(obj, interceptor));
}
/**
* 代理方法核心
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//判断插件上的注解是否与method的方法名匹配(需拦截executor的哪个一个方法)
if (interceptor.getClass().isAnnotationPresent(com.test.mybatis.v2.annotation.Method.class)) {
if (method.getName().equals(interceptor.getClass().getAnnotation(com.test.mybatis.v2.annotation.Method.class).value())) {
//不执行原方法,直接执行插件方法
return interceptor.intercept(new Invocation(target, method, args));
}
}
return method.invoke(target, method, args);
}
}
接下来编写测试类进行测试吧
SqlSessionFactory factory = new SqlSessionFactory();
//没有插件且没有开启缓存
CustomSqlSession sqlSession1 = factory.build("com.test.mybatis.v2.mapper").openSqlSession();
TestCustomMapper mapper1 = sqlSession1.getMapper(TestCustomMapper.class);
Test test1 = mapper1.selectByPrimaryKey(1);
System.out.println("第一次查询结果为: " + test1);
test1 = mapper1.selectByPrimaryKey(1);
System.out.println("第二次查询结果为: " + test1);
控制台打印
第二个测试
SqlSessionFactory factory = new SqlSessionFactory();
//有插件但没有开启缓存
CustomSqlSession sqlSession2 = factory.build("com.test.mybatis.v2.mapper",
new String[] {"com.test.mybatis.v2.plugin.customPlugin.testPlugin"}).openSqlSession();
TestCustomMapper mapper2 = sqlSession2.getMapper(TestCustomMapper.class);
Test test2 = mapper2.selectByPrimaryKey(2);
System.out.println("第一次查询结果为: " + test2);
test2 = mapper2.selectByPrimaryKey(2);
System.out.println("第二次查询结果为: " + test2);
控制台打印
第三个测试
SqlSessionFactory factory = new SqlSessionFactory();
//有插件且有缓存
CustomSqlSession sqlSession3 = factory.build("com.test.mybatis.v2.mapper",
new String[] {"com.test.mybatis.v2.plugin.customPlugin.testPlugin"}, true).openSqlSession();
TestCustomMapper mapper3 = sqlSession3.getMapper(TestCustomMapper.class);
Test test3 = mapper3.selectByPrimaryKey(3);
System.out.println("第一次查询结果为: " + test3);
test3 = mapper3.selectByPrimaryKey(3);
System.out.println("第二次查询结果为: " + test3);
控制台打印
其中plugin作用在executor的query方法之前,Mybatis不仅可以配置插件到executor的query中,还能配置插件到例如ResultSetHandler中。
以上图片为Mybatis官网中截取,可以看到拦截的方法可以有很多种,这里简化了一下,只拦截了executor的query方法,体会到Mybatis的思想即可。
到这里就大致完成了MyBatis2.0版本的编写了,依然存在很多不足之处,可见编写一个强大的框架是十分不易的,不可能一步登天就完成所有的功能,都是一点一点累加上去,正因为如此,一个好的设计理念,好的抽象思维就显得格外重要,需要架构者具有能设计出一个易扩展、符合面向对象设计原则的代码的能力,而这个能力实属不易,或许这就是架构师稀少的原因吧。
总结
不足之处与需要改进的地方
- 设置参数与结果映射都需要一个TypeHandler,这里偷懒简化了,可以作为下一个版本的加强点。
- SQL语句局限于查询方法,新增所有的增删改方法也是下个版本的加强点。
- 实现一个清空缓存的方法,在使用了增删改方法时有清空缓存的必要。
- Plugin插件功能扩展,使其可以作用到Executor以外的三个类,扩展可以拦截的方法。
以上提到的功能除了TypeHandler以外,都是在做重复工作,意义不大,所以我在2.0版本中没有去实现那些功能,例如增删改查方法我只实现一个查的方法, 作为一个例子,其他的增删改都是可以一样画葫芦的,Plugin功能扩展也是如此。
写到这里,越发觉得开发一个框架例如mybatis是多么的不易,感觉到mybatis底下组件的强大,2.0中一定还有很多没有提及的缺点,可以体会开发一个健壮的框架的难度了。经过这次自己手写MyBatis,更深刻的认识了MyBatis底层各种组件的实现原理,也巩固了一些设计模式、面向对象的一些原则。