java实现mybatis的分表插件

java实现mybatis的分表插件

当系统数据量达到瓶颈时,我们一般会采取分表的方式来存储数据。但是Mybatis自身不具备分表查询的能力。所以,本文将使用java与Mybatis提供的插件机制来实现一个分表插件

了解Mybatis的Interceptor接口

package org.apache.ibatis.plugin;

import java.util.Properties;

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

这3个函数代表什么

intercept() --- 拦截器接口,实现了此接口,mybatis在初始化时,就会将我们自定义的实现类注册到插件集合里去。类似于spring的HandlerInterceptor接口

plugin() --- 代理生成函数。

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

setProperties() --- 自定义配置文件。在xml中为插件配置的一些可变参数项。如下代码,会解析我们在xml中配置的节点信息,然后将配置的properties设置到拦截器中

  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

ThreadLocal的简单认识

用于存储线程变量,但是与其他线程完全隔离,存储的数据,只归当前线程所有。和request.session类似。每一个Session都只隶属于当前request,那么我们在全局里获取Session中的数据就很方便了,无需通过参数传递

了解了基本过程,下面开始开发分表插件

TableShard.java

用以标记类,便于反射获取

/**
 * 分表标记 针对类上的注解,作用域是所有接口
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableShard {

    /**
     * 要替换的表名列表 默认替换所有表名 如果存在替换部分 请填写需要替换的表名在数组中
     */
    String[] tableNames() default {};

    // 对应的分表策略类
    Class<? extends IStrategy> shardStrategy() default DefaultShardStrategy.class;

    String tableName() default "";
}

FlexibleShardTable.java

自定义不同表的不同分表策略

/**
 * 作用于是Method  针对单独的Mapper接口定制化分表策略
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FlexibleShardTable {

    TableShard[] moreTableStrategy() default {};
}

IStrategy.java

分表策略接口

public interface IStrategy<T> {
    String strategyHandler(T t);
}

DefaultShardStrategy.java

默认的分表策略

/**
 * 根据ID取10的模
 */
public class DefaultShardStrategy implements IStrategy<Integer>{
    @Override
    public String calculate(Integer o) {
        return (o % 10) + "";
    }
}

ShardHelper.java

辅助类,分表操作时的一些共享变量存取,清理等工作

public class ShardHelper {

    private static ThreadLocal<ShardModel> localShardModel = new ThreadLocal<>();

    /**
     * 获取线程变量
     */
    public static ShardModel getLocalShardModel() {
        return localShardModel.get();
    }

    /**
     * 设置分表参数
     * @param id
     */
    public static void setShardId(Long id){
        localShardModel.set(new ShardModel(id));
    }

    /**
     * 清理
     */
    public static void clear(){
        localShardModel.remove();
    }
}

ShardModel.java

线程变量对象,内置分表时需要的参数属性

public class ShardModel {

    public ShardModel(Long shopId) {
        this.id = shopId;
    }

    private Long id;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

ReflectionUtils.java

反射工具类 (网上找的工具类,主要用于反射获取一些私有属性。不做过多的介绍)

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import java.lang.reflect.*;

public class ReflectionUtils {
    private static Logger logger = LoggerFactory.getLogger(ReflectionUtils.class);

    /**
     * 调用Getter方法.
     */
    public static Object invokeGetterMethod(Object obj, String propertyName) {
        String getterMethodName = "get" + StringUtils.capitalize(propertyName);
        return invokeMethod(obj, getterMethodName, new Class[] {}, new Object[] {});
    }

    /**
     * 调用Setter方法.使用value的Class来查找Setter方法.
     */
    public static void invokeSetterMethod(Object obj, String propertyName, Object value) {
        invokeSetterMethod(obj, propertyName, value, null);
    }

    /**
     * 调用Setter方法.
     *
     * @param propertyType 用于查找Setter方法,为空时使用value的Class替代.
     */
    public static void invokeSetterMethod(Object obj, String propertyName, Object value, Class<?> propertyType) {
        Class<?> type = propertyType != null ? propertyType : value.getClass();
        String setterMethodName = "set" + StringUtils.capitalize(propertyName);
        invokeMethod(obj, setterMethodName, new Class[] { type }, new Object[] { value });
    }

    /**
     * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
     */
    public static Object getFieldValue(final Object obj, final String fieldName) {
        Field field = getAccessibleField(obj, fieldName);

        if (field == null) {
            throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + obj + "]");
        }

        Object result = null;
        try {
            result = field.get(obj);
        } catch (IllegalAccessException e) {
            logger.error("不可能抛出的异常{}", e.getMessage());
        }
        return result;
    }

    /**
     * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数.
     */
    public static void setFieldValue(final Object obj, final String fieldName, final Object value) {
        Field field = getAccessibleField(obj, fieldName);

        if (field == null) {
            throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + obj + "]");
        }

        try {
            field.set(obj, value);
        } catch (IllegalAccessException e) {
            logger.error("不可能抛出的异常:{}", e.getMessage());
        }
    }

    /**
     * 循环向上转型, 获取对象的DeclaredField,   并强制设置为可访问.
     *
     * 如向上转型到Object仍无法找到, 返回null.
     */
    public static Field getAccessibleField(final Object obj, final String fieldName) {
        Assert.notNull(obj, "object不能为空");
        Assert.hasText(fieldName, "fieldName");
        for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
            try {
                Field field = superClass.getDeclaredField(fieldName);
                field.setAccessible(true);
                return field;
            } catch (NoSuchFieldException e) {//NOSONAR
                // Field不在当前类定义,继续向上转型
            }
        }
        return null;
    }

    /**
     * 直接调用对象方法, 无视private/protected修饰符.
     * 用于一次性调用的情况.
     */
    public static Object invokeMethod(final Object obj, final String methodName, final Class<?>[] parameterTypes,
                                      final Object[] args) {
        Method method = getAccessibleMethod(obj, methodName, parameterTypes);
        if (method == null) {
            throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + obj + "]");
        }
        try {
            return method.invoke(obj, args);
        } catch (Exception e) {
            throw convertReflectionExceptionToUnchecked(e);
        }
    }

    /**
     * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问.
     * 如向上转型到Object仍无法找到, 返回null.
     *
     * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args)
     */
    public static Method getAccessibleMethod(final Object obj, final String methodName,
                                             final Class<?>... parameterTypes) {
        Assert.notNull(obj, "object不能为空");
        for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
            try {
                Method method = superClass.getDeclaredMethod(methodName, parameterTypes);
                method.setAccessible(true);
                return method;
            } catch (NoSuchMethodException e) {//NOSONAR
                // Method不在当前类定义,继续向上转型
            }
        }
        return null;
    }

    public static Method getAccessibleMethod(final Class clz,final String methodName,final Class<?> ... parameterTypes) throws ClassNotFoundException {
        Assert.notNull(clz, "class不能为空");
        for (Class<?> superClass = clz; (superClass != Object.class && superClass != null); superClass = superClass.getSuperclass()) {
            try {
                Method method = superClass.getDeclaredMethod(methodName, parameterTypes);
                method.setAccessible(true);
                return method;
            } catch (NoSuchMethodException e) {//NOSONAR
                // Method不在当前类定义,继续向上转型
            }
        }
        return null;
    }

    /**
     * 根据方法名称取得反射方法的参数类型(没有考虑同名重载方法使用时注意)
     * @param clazz       类
     * @param methodName  方法名
     */
    public static Class[] getMethodParamTypes(Class clazz,
                                               String methodName) throws ClassNotFoundException{
        Class[] paramTypes = null;
        Method[]  methods = clazz.getMethods();//全部方法
        for (int  i = 0;  i< methods.length; i++) {
            if(methodName.equals(methods[i].getName())){//和传入方法名匹配
                Class[] params = methods[i].getParameterTypes();
                paramTypes = new Class[ params.length] ;
                for (int j = 0; j < params.length; j++) {
                    paramTypes[j] = Class.forName(params[j].getName());
                }
                break;
            }
        }
        return paramTypes;
    }

    /**
     * 通过反射, 获得Class定义中声明的父类的泛型参数的类型.
     * 如无法找到, 返回Object.class.
     * eg.
     * public UserDao extends HibernateDao<User>
     *
     * @param clazz The class to introspect
     * @return the first generic declaration, or Object.class if cannot be determined
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static <T> Class<T> getSuperClassGenricType(final Class clazz) {
        return getSuperClassGenricType(clazz, 0);
    }

    /**
     * 通过反射, 获得Class定义中声明的父类的泛型参数的类型.
     * 如无法找到, 返回Object.class.
     *
     * 如public UserDao extends HibernateDao<User,Long>
     *
     * @param clazz clazz The class to introspect
     * @param index the Index of the generic ddeclaration,start from 0.
     * @return the index generic declaration, or Object.class if cannot be determined
     */
    @SuppressWarnings("rawtypes")
    public static Class getSuperClassGenricType(final Class clazz, final int index) {
        Type genType = clazz.getGenericSuperclass();
        if (!(genType instanceof ParameterizedType)) {
            logger.warn(clazz.getSimpleName() + "'s superclass not ParameterizedType");
            return Object.class;
        }
        Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
        if (index >= params.length || index < 0) {
            logger.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: "
                    + params.length);
            return Object.class;
        }
        if (!(params[index] instanceof Class)) {
            logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter");
            return Object.class;
        }
        return (Class) params[index];
    }

    /**
     * 将反射时的checked exception转换为unchecked exception.
     */
    public static RuntimeException convertReflectionExceptionToUnchecked(Exception e) {
        if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException
                || e instanceof NoSuchMethodException) {
            return new IllegalArgumentException("Reflection Exception.", e);
        } else if (e instanceof InvocationTargetException) {
            return new RuntimeException("Reflection Exception.", ((InvocationTargetException) e).getTargetException());
        } else if (e instanceof RuntimeException) {
            return (RuntimeException) e;
        }
        return new RuntimeException("Unexpected Checked Exception.", e);
    }
}

TableShardInterceptor.java

核心类,实现分表的拦截器实现

@Intercepts
        ({      //请注意这里存在的问题,低版本 StatementHandler 的 prepare 函数只有一个参数 Connection 那么args = {Connection.class} 即可
                //高版本中,StatementHandler 的 prepare 函数存在二个参数(不知道开发者为啥不选择重载而选择重写) 所以这里的args = {Connection.class,Integer.class}
                @Signature(type = StatementHandler.class,method = "prepare",args = { Connection.class,Integer.class})
        })
@Component
public class TableShardInterceptor implements Interceptor {

    private static Logger logger = LoggerFactory.getLogger(TableShardInterceptor.class);

    private static String dbType = JdbcConstants.MYSQL;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            //Target包含了Mybatis线程中的上下文实例
            RoutingStatementHandler routingStatementHandler = (RoutingStatementHandler)invocation.getTarget();
            // 四大对象中最重要的一个,用语于数据库直接交互的接口,从中我们可以获取到sql语句与传递参数
            StatementHandler statementHandler = (StatementHandler) ReflectionUtils.getFieldValue(routingStatementHandler, "delegate");
            // 获取Mapper接口上的注解
            TableShard tableShard = parserClassShard(statementHandler);
            FlexibleShardTable flexibleShardTable = parserMethodShard(statementHandler);
            //如果不存在分表标记,则直接放行
            if(tableShard == null){
                return invocation.proceed();
            }

            //获取线程变量
            ShardModel shardModel = ShardHelper.getLocalShardModel();
            if(shardModel == null){
                throw new Exception("shard table model must not empty");
            }

            //这儿使用id进行分表,具体可以按照自己的业务设计
            Long id = shardModel.getId();

            if(id == null){
                throw new Exception("shard table column must not null -->{id}");
            }

            // 如果存在定制化的分表策略,与Class级别的分表策略合并
            Map<String,Class<? extends IStrategy>> strategyMap = mergeTableInfo(tableShard,flexibleShardTable,statementHandler);

            //循环执行,将sql中的表名替换为我们重新计算后的表名
            strategyMap.keySet().forEach(tableName -> {
                Class<? extends IStrategy> strategyClazz = strategyMap.get(tableName);
                IStrategy<Long,String> strategy;
                try {
                    strategy = strategyClazz.newInstance();
                    String newTableName = strategy.strategyHandler(id);
                    replaceTableName(tableName,newTableName,statementHandler);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }catch (Exception e){
           logger.error("【mybatis Interceptor】 {}" , e.getMessage());
            throw e;
        }

        return invocation.proceed();
    }

    private Map<String,Class<? extends IStrategy>> mergeTableInfo(TableShard tableShard, FlexibleShardTable flexibleShardTable,StatementHandler statementHandler) throws Exception {

        Map<String,Class<? extends IStrategy>> strategyMap = new HashMap<>();

        if(tableShard != null){
            //Class级别的注解不为空 解析策略类构造器
            strategyMap.putAll(parserClassShardTableStrategy(tableShard,statementHandler));
        }

        if(flexibleShardTable != null){
            //Method级别的注解不为空,解析策略类构造器
            strategyMap.putAll(parserClassMethodShardTableStrategy(flexibleShardTable));
        }

        //合并之后的集合
        return strategyMap;
    }

    private Map<? extends String, Class<? extends IStrategy>> parserClassMethodShardTableStrategy(FlexibleShardTable flexibleShardTable) throws Exception {

        Map<String,Class<? extends IStrategy>> result = new HashMap<>();

        TableShard[] tableShards = flexibleShardTable.moreTableStrategy();

        for (TableShard tableShard : tableShards) {
            result.put(tableShard.tableName(),tableShard.shardStrategy());
        }
        return result;
    }

    private Map<? extends String, Class<? extends IStrategy>> parserClassShardTableStrategy(TableShard tableShard,StatementHandler statementHandler) throws Exception {
        Map<String,Class<? extends IStrategy>> result = new HashMap<>();

        String[] tableNames = tableShard.tableNames();
        if(tableShard.tableNames().length == 0){
            //如果没指定表名,则解析sql中所有的表名称
            tableNames = parserOriginalTableNames(statementHandler);
        }

        for (String tableName : tableNames) {
            result.put(tableName,tableShard.shardStrategy());
        }

        if(!tableShard.tableName().isEmpty()){
            //继续解析tableName
            result.put(tableShard.tableName(),tableShard.shardStrategy());
        }

        return result;
    }

    /**
     * 获取类的分表注解
     */
    private TableShard parserClassShard(StatementHandler statementHandler) throws Exception {
        Class classProxy = getClassProxy(statementHandler);
        return (TableShard)classProxy.getAnnotation(TableShard.class);
    }

    /**
     * 获取函数的分表注解
     */
    private FlexibleShardTable parserMethodShard(StatementHandler statementHandler) throws Exception {
        Class classProxy = getClassProxy(statementHandler);

        String classId = getClassId(statementHandler);
        //获取Method名称
        String methodName = classId.substring(classId.lastIndexOf(".") + 1);
        //获取Method句柄
        Method method = ReflectionUtils.getAccessibleMethod(classProxy,methodName,ReflectionUtils.getMethodParamTypes(classProxy,methodName));
        if(method == null){
            return null;
        }
        return method.getAnnotation(FlexibleShardTable.class);
    }

    /**
     * 获取访问者代理 就是我们的Mapper类构造器
     */
    private Class getClassProxy(StatementHandler statementHandler) throws Exception {
        String id = getClassId(statementHandler);
        id = id.substring(0, id.lastIndexOf('.'));

        return Class.forName(id);
    }
    /**
     * Mapper类的相对路径
     */
    private String getClassId(StatementHandler statementHandler) throws Exception {
        MappedStatement mappedStatement = (MappedStatement)getObjFiled(statementHandler.getParameterHandler(),"mappedStatement").get(statementHandler.getParameterHandler());

        if(mappedStatement == null){
            return null;
        }

        return mappedStatement.getId();
    }

    /**
     * 解析当前执行的原始sql语句
     */
    private String parserSql(StatementHandler statementHandler) throws Exception {
        BoundSql boundSql = (BoundSql)getObjFiled(statementHandler.getParameterHandler(),"boundSql").get(statementHandler.getParameterHandler());
        return boundSql.getSql();
    }

    /**
     * 替换原始表名
     */
    private void replaceTableName(String oldTableName,String newTableName, StatementHandler statementHandler) throws Exception {

        //获取原始sql语句
        String sql = parserSql(statementHandler);

        // 获取boundSql对象,这个对象是存储sql语句的,最重要的就是将这个对象中的sql更换
        Field boundField = getObjFiled(statementHandler.getParameterHandler(),"boundSql");
        BoundSql boundSql = (BoundSql)boundField.get(statementHandler.getParameterHandler());

        //获取sql属性
        Field sqlField = getObjFiled(boundSql,"sql");
        //替换sql中的表名称,并且设置到获取boundSql对象中
        sqlField.set(boundSql,sql.replaceAll(oldTableName,oldTableName + newTableName));
    }

    private String[] parserOriginalTableNames(StatementHandler statementHandler) throws Exception {
        //获取sql语句
        String sql = parserSql(statementHandler);
        //获取sql中的所有表名 这儿使用了alibaba.druid.sql 的工具类,对比了很多工具类,这个最优
        String[] tableNames = getTableNameBySql(sql);

        if(tableNames.length == 0 ){
            throw new Exception("Not Parser Anywhere TableName");
        }
        //返回所有表名
        return tableNames;
    }

    private static String[] getTableNameBySql(String sql){

        String result = SQLUtils.format(sql, dbType);
        logger.info("格式化后输出:\n" + result);
        logger.info("*********************");
        List<SQLStatement> sqlStatementList = getSQLStatementList(sql);
        //默认为一条sql语句
        SQLStatement stmt = sqlStatementList.get(0);
        MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
        stmt.accept(visitor);
        logger.info("数据库类型\t\t" + visitor.getDbType());
        logger.info("查询的字段\t\t" + visitor.getColumns());
        logger.info("表名\t\t\t" + visitor.getTables().keySet());
        logger.info("条件\t\t\t" + visitor.getConditions());
        logger.info("group by\t\t" + visitor.getGroupByColumns());
        logger.info("order by\t\t" + visitor.getOrderByColumns());


        Set<TableStat.Name> tableNames = visitor.getTables().keySet();
        String[] tableArray = new String[tableNames.size()];

        int i = 0;
        for (TableStat.Name tableName : tableNames) {
            tableArray[i] = tableName.getName();
            i ++ ;
        }

        return tableArray;
    }

    private static List<SQLStatement> getSQLStatementList(String sql) {
        return SQLUtils.parseStatements(sql, dbType);
    }

    private Field getObjFiled(Object statementHandler,String fieldName) throws NoSuchFieldException {
        Field field = statementHandler.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        return field;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

执行流程

  • 1 通过Invocation对象获取Mybatis上下文,拿到最重要的对象StatementHandler

  • 2 通过StatementHandler对象获取访问者代理类(也就是我们的Mapper接口)

  • 3 拿到Mapper类构造器后,反射获取类上的分表注解(TableShard),存在则继续执行,不存在则方行不做任何操作

  • 4 通过ShardModel获取线程变量,然后通过分表注解(TableShard)获取分表策略,计算出新的表名

  • 5 通过StatementHandler反射获取到BoundSql对象,替换BoundSql中的sql属性值。

  • 6 在经历了分表拦截器的操作后,BoundSql中的sql语句表名已被我们成功替换成新的表名,然后放行,交给StatementHandler与数据库执行

使用示例 StudentMapper.java

//添加分表注解
@TableShard
public interface StudentMapper {
    int deleteByPrimaryKey(Long id);

    int insert(Student record);

    int insertOrUpdate(Student record);

    int insertOrUpdateSelective(Student record);

    int insertSelective(Student record);

    Student selectByPrimaryKey(Long id);

    int updateByPrimaryKeySelective(Student record);

    int updateByPrimaryKey(Student record);

    int updateBatch(List<Student> list);

    int batchInsert(@Param("list") List<Student> list);
}

TableApplicationTests.java

调用时,记得设置分表id,调用完毕后,记得清理 (也可以自己在拦截器中清理,全凭个人喜好)

@RunWith(SpringRunner.class)
@SpringBootTest
public class TableApplicationTests {

    @Autowired
    private StudentMapper studentMapper;

    @Test
    public void contextLoads() {

        /**
         * <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
         *     select
         *     <include refid="Base_Column_List" />
         *     from student
         *     where id = #{id,jdbcType=BIGINT}
         *   </select>
         */

        //设置分表ID
        ShardHelper.setShardId(154154L);

        Student student = studentMapper.selectByPrimaryKey(1L);
        System.out.println(student.toString());
        //清理
        ShardHelper.clear();

    }

}

输出日志

2019-08-13 11:18:48.473  INFO 20996 --- [           main] c.s.d.t.s.i.TableShardInterceptor        : *********************
2019-08-13 11:18:48.523  INFO 20996 --- [           main] c.s.d.t.s.i.TableShardInterceptor        : 数据库类型		mysql
2019-08-13 11:18:48.523  INFO 20996 --- [           main] c.s.d.t.s.i.TableShardInterceptor        : 查询的字段		[student.id, student.name, student.age, student.rule]
2019-08-13 11:18:48.523  INFO 20996 --- [           main] c.s.d.t.s.i.TableShardInterceptor        : 表名			[student]
2019-08-13 11:18:48.523  INFO 20996 --- [           main] c.s.d.t.s.i.TableShardInterceptor        : 条件			[student.id = null]
2019-08-13 11:18:48.523  INFO 20996 --- [           main] c.s.d.t.s.i.TableShardInterceptor        : group by		[]
2019-08-13 11:18:48.523  INFO 20996 --- [           main] c.s.d.t.s.i.TableShardInterceptor        : order by		[]
2019-08-13 11:18:48.524  INFO 20996 --- [           main] c.s.d.t.s.i.TableShardInterceptor        : 执行分表策略参数: 154154
2019-08-13 11:18:48.526  INFO 20996 --- [           main] c.s.d.t.s.i.TableShardInterceptor        : 原始表名:(student)   新表名:(student_4)   策略:(com.shard.demo.table.shardplugin.strategy.DefaultShardStrategy)
==>  Preparing: select id, `name`, age, `rule` from student_4 where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, name, age, rule
<==        Row: 1, 学生4, 40, 清洁委员
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f91fbda]
Student{id=1, name='学生4', age=40, rule='清洁委员'}
2019-08-13 11:18:48.578  INFO 20996 --- [       Thread-4] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
2019-08-13 11:18:48.582  INFO 20996 --- [       Thread-4] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} closed

由日志可以看出,student已经被替换成student_4,并且成功执行查询操作

完整代码git地址

# java  mybatis 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×