Mybatis之拒绝重复代码

Posted 抬头望月日已出

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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));
      
      以上是关于Mybatis之拒绝重复代码的主要内容,如果未能解决你的问题,请参考以下文章

Java语言基础之方法的设计

Mybatis动态sql

Android应用安全之Android APP通用型拒绝服务漏洞

48个值得掌握的JavaScript代码片段(上)

MyBatis之Mapper XML 文件详解-sql和入参

Mybatis -- 动态Sql概述动态Sql之<if>(包含<where>)动态Sql之<foreach>sql片段抽取