手写MyBatis2.0附带Plugin功能(增强版本)

基于上一篇博客,手写MyBatis1.0末尾提出的几个不足之处与需要新增的地方,这篇博客将完善之前的MyBatis1.0版本,升级为2.0版本~将会新增的功能:

  1. 加入Plugin插件功能。
  2. 加入缓存功能。
  3. 分解Executor指责,分出各个类使其符合单一职责原则。
  4. 使用注解灵活配置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);

控制台打印

手写MyBatis2.0附带Plugin功能(增强版本)

第二个测试

        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);

控制台打印

手写MyBatis2.0附带Plugin功能(增强版本)

第三个测试

        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);

控制台打印

手写MyBatis2.0附带Plugin功能(增强版本)

其中plugin作用在executor的query方法之前,Mybatis不仅可以配置插件到executor的query中,还能配置插件到例如ResultSetHandler中。

手写MyBatis2.0附带Plugin功能(增强版本)

以上图片为Mybatis官网中截取,可以看到拦截的方法可以有很多种,这里简化了一下,只拦截了executor的query方法,体会到Mybatis的思想即可。

到这里就大致完成了MyBatis2.0版本的编写了,依然存在很多不足之处,可见编写一个强大的框架是十分不易的,不可能一步登天就完成所有的功能,都是一点一点累加上去,正因为如此,一个好的设计理念,好的抽象思维就显得格外重要,需要架构者具有能设计出一个易扩展、符合面向对象设计原则的代码的能力,而这个能力实属不易,或许这就是架构师稀少的原因吧。

总结

不足之处与需要改进的地方

  1. 设置参数与结果映射都需要一个TypeHandler,这里偷懒简化了,可以作为下一个版本的加强点。
  2. SQL语句局限于查询方法,新增所有的增删改方法也是下个版本的加强点。
  3. 实现一个清空缓存的方法,在使用了增删改方法时有清空缓存的必要。
  4. Plugin插件功能扩展,使其可以作用到Executor以外的三个类,扩展可以拦截的方法。

以上提到的功能除了TypeHandler以外,都是在做重复工作,意义不大,所以我在2.0版本中没有去实现那些功能,例如增删改查方法我只实现一个查的方法, 作为一个例子,其他的增删改都是可以一样画葫芦的,Plugin功能扩展也是如此。

写到这里,越发觉得开发一个框架例如mybatis是多么的不易,感觉到mybatis底下组件的强大,2.0中一定还有很多没有提及的缺点,可以体会开发一个健壮的框架的难度了。经过这次自己手写MyBatis,更深刻的认识了MyBatis底层各种组件的实现原理,也巩固了一些设计模式、面向对象的一些原则。