Mybatis之拒绝重复代码

***Provider的使用

当我们在使用Mybatis时,会发现每个类里面的方法都是重复的,比如add,update,selectById等等,我们总不至于在没个mapper里面都要这样add,update,重复的无意义工作。(mybatis-plus真香).

mybatis提供了一个一种可以在mapper接口方法上面定义的注解
在这里插入图片描述
在这里插入图片描述
源码中可以看到示例。然后新建接口写一些公共方法。

public interface BaseMapper<T> {

     @InsertProvider(type = SqlProvider.class, method = "insert")
     int add(T t);

     @UpdateProvider(type = SqlProvider.class,method="modify")
     int update(T t);

     class SqlProvider{

         public static <T> String insert(T t) {
             Class<?> aClass = t.getClass();
             String simpleName = aClass.getSimpleName();
             Field[] declaredFields = aClass.getDeclaredFields();
             //组装inset into
             StringBuffer pre = new StringBuffer("insert into ");
             pre.append(simpleName).append("(");
             List<Field> fields = Arrays.stream(declaredFields).collect(Collectors.toList());
             fields.forEach(e->{
                 //驼峰转换下划线
                 String field = TransformUtil.ToLowerLine(e.getName());
                 pre.append(field).append(",");
             });
             String prefixSql=pre.substring(0, pre.length() - 1)+")";
             //组装values
             StringBuffer value=new StringBuffer("VALUES (");
             fields.forEach(e->{
                 value.append("#{").append(e.getName()).append("},");
             });
             String suffixSql = value.substring(0, value.length() - 1)+") ";
             return prefixSql+suffixSql;
         }

         public  static <T> String modify(T t){
             Class<?> aClass = t.getClass();
             String simpleName = aClass.getSimpleName();
             StringBuffer pre=new StringBuffer("update  ");
             pre.append(simpleName).append(" set ");
             Field[] declaredFields = aClass.getDeclaredFields();
             for (int i = 1; i <declaredFields.length ; i++) {
                 //驼峰转换下划线
                 String field = TransformUtil.ToLowerLine(declaredFields[i].getName());
                 pre.append(field).append("=").append("#{").append(declaredFields[i].getName()).append("},");
             }
             String prefixSql = pre.substring(0, pre.length() - 1);
             prefixSql+=" where id=#{id}";
             return prefixSql;
         }
     }
}

这里通过反射去处理sql。

然后叫dao继承此BaseMapper

public interface CarDao extends BaseMapper<Car> 

实体信息如下:

@Data
public class Car {

    private Integer id;

    private String name;

    private LocalDateTime createTime;

    private String creator;

    private LocalDateTime updateTime;

    private String modified;

}

下面执行add和update。

   public static  void testProvider(SqlSessionFactory sqlSessionFactory) {
        SqlSession session = sqlSessionFactory.openSession();
        CarDao carDao = session.getMapper(CarDao.class);

        Car car = new Car();
        car.setName("first");
        carDao.add(car);

        car.setId(1);
        carDao.update(car);

        session.commit();
    }

打印执行日志

==>  Preparing: insert into Car(id,name,create_time,creator,update_time,modified)VALUES (?,?,?,?,?,?)
==> Parameters: null, first(String), 2021-11-30T10:09:26.939(LocalDateTime), boss(String), null, null
<==    Updates: 1
==>  Preparing: update Car set name=?,create_time=?,creator=?,update_time=?,modified=? where id=?
==> Parameters: first(String), 2021-11-30T10:09:26.939(LocalDateTime), boss(String), 2021-11-30T10:09:27.747(LocalDateTime), 修改者(String), 1(Integer)
<==    Updates: 1

在这里插入图片描述
可以看到数据中已经插入数据。

mybatis在扫描mappers后,会找到xml地址(resource方式),解析里面的标签,通过namespace地址找到接口地址,获取所有方法,这些有着特殊注解的方法,也会解析成MappedStatement。在mybatis中每一个dao层的方法的限定名对应一个MappedStatement(里面存放的是方法的信息,比如你的sql语句信息,sql类型,参数等等),如果我们在执行dao层方法时没有找到对应的MappedStatement,就会抛出异常:Invalid bound statement (not found)。那就说明我们没写接口方法的实现。

通过Provider可以实现很多公共方法,比如deleteById,selectById等等。

插件的应用

细心的话会发现,我们上面在执行add,update时打印的sql,createTime,creator,updateTime,modified分别在执行add和update时是有值的。
这个是怎么做到的呢?

在这里插入图片描述

mybatis提供了四个类用于不同时间进行拦截。
现在要在执行插入和更新的时候要可以给类进行赋值,要怎么做。
首先要确定我们传的参数在什么位置可以获取到,并且能够确定方法是不是我们要拦截的。

  public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      //这个方法就是我们要拦截的
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

在mybatis源码中可以看到excutor在执行update时可以获取到MappedStatement和parameter(我们的参数)。
代码如下:

@Intercepts(@Signature(type = Executor.class,method = "update",args = {MappedStatement.class,Object.class}))
public class ExecutorPlug implements Interceptor {

    /**
     * 要拦截的方法
     */
    private static  final List<String> ARR = Stream.of("add","update").collect(Collectors.toList());

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement statement=null;
        for (Object obj: args ) {
            if(obj instanceof MappedStatement){
                statement=(MappedStatement)obj;
            }else{
                //这个id是方法的限定名
                String id = statement.getId();
                String[] split = id.split("\.");
                String methodName=split[split.length-1];
                //方法是否是我们自定义的方法
                if(ARR.contains(methodName)){
                    SqlCommandType sqlCommandType = statement.getSqlCommandType();
                    //sql类型
                    switch (sqlCommandType){
                        case INSERT:
                            insert(obj);
                            break;
                        case UPDATE:
                            update(obj);
                            break;
                        default:
                            break;
                    }
                }
            }
        }
        return invocation.proceed();
    }

    public void insert(Object obj) throws NoSuchFieldException, IllegalAccessException {
            Class<?> aClass = obj.getClass();
            Field createDate = aClass.getDeclaredField("createTime");
            createDate.setAccessible(true);
            createDate.set(obj, LocalDateTime.now());
            createDate.setAccessible(false);
            Field creator = aClass.getDeclaredField("creator");
            creator.setAccessible(true);
            creator.set(obj,"boss");
            creator.setAccessible(false);
    }

    public void update(Object obj) throws NoSuchFieldException, IllegalAccessException {
        Class<?> aClass = obj.getClass();
        Field createDate = aClass.getDeclaredField("updateTime");
        createDate.setAccessible(true);
        createDate.set(obj, LocalDateTime.now());
        createDate.setAccessible(false);
        Field creator = aClass.getDeclaredField("modified");
        creator.setAccessible(true);
        creator.set(obj,"修改者");
        creator.setAccessible(false);
    }

}

插件定义好了,但是需要叫mybatis扫描到,所以需要在配置文件中加入plugin配置

        <plugin interceptor="org.example.plugins.ExecutorPlug">
            <property name="testPlug" value="100"/>
        </plugin>

这样我们在执行公共方法时就不要做这些重复的设置这些属性的值了。

下面是Mybatis怎么扫描配置文件的,可以看到如果不配置是不生效的。

  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      //加载插件
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

mybatis在扫描时会通过标签名称进行筛选,执行不同的操作,可以看到里面有环境配置,mappers等。这些信息会保存configuration类中,这个类在mybatis中贯彻全局,我们所有的信息都是从这个里面获取的。

那执行器是在什么时间创建的呢?
当我们在 sqlSessionFactory.openSession()时会创建一个执行器。

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

mybatis是怎么实现这些责任链的调用呢?
在newExecutor中有一句interceptorChain.pluginAll(executor);

 public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
  //最终调用的方法
  public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }
  //代理类执行的方法
    @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
      //这里去调用具体的插件实现
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

可以发现里面使用JDK的代理实现的。
这样插件在执行到特定方法时就是先走最外层的代理,然后执行method.invoke(),交给他的下级代理,直到最后执行真正的方法。

dao层方法的执行

dao接口在执行方法,也是通过jdk的代理实现的。当我们在获取dao时mybatis会返回一个MapperProxy

  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

我们在执行的时候会进到invoke方法,然后在configuration中的mappedStatements根据方法名获取MappedStatement。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码

)">
< <上一篇

)">
下一篇>>