mybatis拦截器源码分析

mybatis拦截器源码分析

拦截器简介

mybatis Plugins 拦截器

由于Mybatis对数据库访问与操作进行了深度的封装,让我们应用开发效率大大提高,但是灵活度很差

拦截器的作用:深度定制Mybatis的开发

抛出一个需求 :获取Mybatis在开发过程中执行的SQL语句(执行什么操作获取那条SQL语句)

​ 在JDBC中我们的sql都会直接定义出来,所以实现上面这个需求很简单.但是在Mybatis中由于深度封装导致不好进行灵活满足需求,所以Mybatis拦截器可以用来解决这一系列问题.

Mybatis拦截器作用

作用:通过拦截器拦截用户对DAO方法的调用,加入一些通用功能(等同于Spring中的AOP操作)


 client ------>  UserDAO.save ----->  处理功能
 				 mybatis拦截器

而我们通过之前的mybatis核心运行流程源码分析得知其实为我们执行增删改查操作的是SqlSession.而SqlSession是依赖Executor,StatementHandler,ParameterHandler,ResultHandler这些mybatis核心对象来进行操作的.

UserDAO.save()		--->	SqlSession.insert()				Executor
UserDAO.update()	--->	SqlSession.update()    =====>   StatementHandler
UserDAO.delete()	--->	SqlSession.delete()				ParameterHandler
UserDAO.findAll()	--->	SqlSession.select()				ResultHandler

所以我们应该拦截的是这些mybatis核心对象,准确说应该是这些对象的方法.而我们比较常用的是Executor,StatementHandler.因为增删改查操作是由StatementHandler,所以StatementHandler是最常用的

拦截器的基本开发

主要分俩步: 1.编码
			1.1 需要实现拦截器的接口(Interceptor)
			1.2 标注需要拦截的目标
		  2.配置

代码如下:

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Properties;
//标注需要拦截的目标为Executor类中的query方法,因为query()方法有2个所以要将具体方法的参数也一起进行标注
@Intercepts({
        @Signature(type= Executor.class,method="query",args={MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class})
})
public class MyMybatisInterceptor implements Interceptor {
    private static final Logger log = LoggerFactory.getLogger(MyMybatisInterceptor.class);

    @Override
    /**
     *  作用:执行的拦截功能 书写在这个方法中.
     *       放行
     */
    public Object intercept(Invocation invocation) throws Throwable {
        if (log.isDebugEnabled())
            log.debug("----拦截器中的 intercept 方法执行------  "+test);
        //执行完拦截功能继续往下执行
        return invocation.proceed();
    }

    /*
     *  把这个拦截器目标 传递给 下一个拦截器(可能存在多个拦截器)
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }

    /*
     *  获取拦截器相关参数的
     */
    @Override
    public void setProperties(Properties properties) {
      
    }
}

然后在将拦截器的配置写在mybatis-config.xml文件中即可(后续代码不在赘述)

<plugins>
       <plugin interceptor="com.baizhiedu.plugins.MyMybatisInterceptor"></plugin>
    </plugins>

以上代码可以实现在查询操作中,执行我们的打印日志,我们如果需要在更新操作的时候进行拦截只需要在标注拦截注解上在添加一个即可

@Intercepts({
        @Signature(type= Executor.class,method="query",args={MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type= Executor.class,method="update",args={MappedStatement.class,Object.class})
})

如果有多个需要拦截的方法,像上面这样一个个标注太过繁琐,我们可以通过query()和update()都会执行的一个方法进行拦截.这里我们拦截StatementHandler中的prepare()方法,然后将每次需要实现Interceptor中的plugin()方法通过装饰器来进行优化

拦截StatementHandler中的prepare()方法

​ 1.query()和update()都需要prepare()

​ 2.获取Connection等同于JDBC

首先定义装饰器类MyMybatisInterceptorAdapter

import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Plugin;

//这里定义一个抽象类只实现Interceptor中的plugin()方法,后面只需要继承这个抽象类即可实现拦截
public abstract class MyMybatisInterceptorAdapter implements Interceptor {
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }

}
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
//这样即可省略plugin()方法的实现
public class MyMybatisInterceptor2 extends MyMybatisInterceptorAdapter {

    private static final Logger log = LoggerFactory.getLogger(MyMybatisInterceptor2.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
         if (log.isDebugEnabled())
            log.debug("----拦截器中的 intercept 方法执行------  "+test);
        return invocation.proceed();
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

然后继续把问题回到之前,如何获取sql语句.通过我们debug上面的代码可以发现在我们实现的intercept()方法参数中就有目标类的信息,而我们之前通过mybatis核心流程源码分析得知boundSql对象中就有一个String类型的变量来存放sql

mybatis拦截器源码分析_第1张图片


import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.util.Properties;

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class MyMybatisInterceptor2 extends MyMybatisInterceptorAdapter {

    private static final Logger log = LoggerFactory.getLogger(MyMybatisInterceptor2.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        
        //RoutingStatementHandler---delegate---
//        RoutingStatementHandler satementHandler = (RoutingStatementHandler) invocation.getTarget();
//        BoundSql boundSql = satementHandler.getBoundSql();
//        String sql = boundSql.getSql();

        //为什么会衍生出第二种写法?因为我们通过debug不一定知道这些对象属性是否私有化,也就是说不一定有get方法,所以通过mybatis底层为我们提供的反射对象MetaObject可以获取到对象的属性值
        MetaObject metaObject = SystemMetaObject.forObject(invocation);
        String sql = (String) metaObject.getValue("target.delegate.boundSql.sql");
        if (log.isDebugEnabled())
            log.debug("sql : " + sql);

        return invocation.proceed();
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

以上的俩种写法都可以实现在执行数据库操作中打印出sql语句

拦截器中获取拦截对象方法相关参数

通过intercept()方法中的Invocation对象,我们进入源码看看

mybatis拦截器源码分析_第2张图片

通过Invocation对象中的args属性即可获取拦截方法的相关参数

详解MeataObject

metaObject —> Mybatis底层封装的反射工具类,便于Mybatis中通过反射操作对象属性

注意:只用引入myabtis依赖才可以使用

		//通过SystemMetaObject.forObject()去获取MetaObject对象,它的参数是你需要获取哪个对象的属性就将对象作为参数传入.我们这里是获取的invocation中的属性,所以传入invocation对象.这样即使对象没有提供get方法我们也能获取到属性值
		MetaObject metaObject = SystemMetaObject.forObject(invocation);
        String sql = (String) metaObject.getValue("target.delegate.boundSql.sql");

mybatis拦截器如何解决开发中的实际问题

在mybatis开发中,可能要对SQL语句进行处理时会使用到拦截器,如分页插件,乐观锁,基于表字段实现的多租户以及逻辑删除等.

分页功能

对于以往的mybatis分页功能,主要采用传参当前页和每页数通过SQL语句中的limit关键字来对数据进行分页,而这种操作会导致代码冗余并且及其不容易维护.通过Mybatis拦截器功能可以拦截要执行的SQL语句进行拼接limit来实现分页这一功能.

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.util.Properties;

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PageHelperInterceptor1 extends MyMybatisInterceptorAdapter {

    private static final Logger log = LoggerFactory.getLogger(MyMybatisInterceptor2.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MetaObject metaObject = SystemMetaObject.forObject(invocation);
        String sql = (String) metaObject.getValue("target.delegate.boundSql.sql");
        //拼接后续的分页查询(这里的0,3只是举个例子实际肯定还是使用当前页变量和每页条数变量)
        String newSql = sql + " limit 0,3";
        //将新的sql语句set进我们的invocation对象中
		metaObject.setValue("target.delegate.boundSql.sql", newSql);
        if (log.isDebugEnabled())
            log.debug("sql : " + sql);
		//然后执行后续操作
        return invocation.proceed();
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

​ 这样就可以简单实现一个分页功能,但是也是存在一些问题的.因为我们这里拦截的方法是StatmentHandler对象中的prepare()方法,增删改查都会去进行拦截.这样就导致了我们进行除了查询的其他操作时,执行的sql语句也会将分页查询给拼接上这样就会导致sql语句语法报错无法执行正常的操作.

这里开始优化判断查询SQL语句,如下:

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.util.Properties;

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PageHelperInterceptor1 extends MyMybatisInterceptorAdapter {

    private static final Logger log = LoggerFactory.getLogger(MyMybatisInterceptor2.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MetaObject metaObject = SystemMetaObject.forObject(invocation);
       String sql = (String) metaObject.getValue("target.delegate.boundSql.sql");
MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("target.delegate.mappedStatement"); 
        String id = mappedStatement.getId();
        //判断当前方法是否是查询方法并且方法末尾为ByPage
        if (id.indexOf("query") != -1 && id.endsWith("ByPage")) {
             String newSql = sql + " limit 0,3 ";
           	 metaObject.setValue("target.delegate.boundSql.sql", newSql);
        }
		//然后执行后续操作
        return invocation.proceed();
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

这里可能在判断直接使用字符串不太合理,具体判断应该由用户来进行自定义设置.

 if (id.indexOf("query") != -1 && id.endsWith("ByPage")) {
 }

我们通过setProperties()这个方法,将具体参数配置到mybatis-config.xml中支持用户自定义字符串

<plugin interceptor="com.baizhiedu.plugins.PageHelperInterceptor1">
            <property name="queryMethodPrefix" value="query"/>
            <property name="queryMethodSuffix" value="ByPage"/>
        plugin>

然后将配置文件的值set进我们定义的成员变量中

//这里省略具体实现方法
 . . .
     
    private String queryMethodPrefix;

    private String queryMethodSuffix;


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MetaObject metaObject = SystemMetaObject.forObject(invocation);

        String sql = (String) metaObject.getValue("target.delegate.boundSql.sql");
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("target.delegate.mappedStatement");
        String id = mappedStatement.getId();
		//通过变量来进行判断,让功能实现变得更加灵活
        if (id.indexOf(queryMethodPrefix) != -1 && id.endsWith(queryMethodSuffix)) {
                String newSql = sql + " limit "+page.getFirstItem()+","+page.getPageCount();

            metaObject.setValue("target.delegate.boundSql.sql", newSql);
        }
        return invocation.proceed();
    }

    @Override
    public void setProperties(Properties properties) {
        //将配置文件中属性值赋值到成员变量中
        this.queryMethodPrefix = properties.getProperty("queryMethodPrefix");
        this.queryMethodSuffix = properties.getProperty("queryMethodSuffix");
    }
}

下面还是对之前的代码进行优化,主要按实际开发流程为主.封装Page对象模拟前端传入参数

public class Page {
    //当前页
    private Integer pageIndex;
    //每页条数
    private Integer pageCount;
    //总条数
    private Integer totalSize;
    //总页数
    private Integer pageSize;

    public Page(Integer pageIndex) {
        this.pageIndex = pageIndex;
        this.pageCount = 5;
    }

    public Page(Integer pageIndex, Integer pageCount) {
        this.pageIndex = pageIndex;
        this.pageCount = pageCount;
    }

    public Integer getPageIndex() {
        return pageIndex;
    }

    public void setPageIndex(Integer pageIndex) {
        this.pageIndex = pageIndex;
    }

    public Integer getPageCount() {
        return pageCount;
    }

    public void setPageCount(Integer pageCount) {
        this.pageCount = pageCount;
    }

    public Integer getTotalSize() {
        return totalSize;
    }

    public void setTotalSize(Integer totalSize) {
        this.totalSize = totalSize;
        if (totalSize % pageCount == 0) {
            this.pageSize = totalSize / pageCount;
        } else {
            this.pageSize = totalSize / pageCount + 1;
        }
    }

    public Integer getPageSize() {
        return pageSize;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }

    // limit getFirstItem,pageSize;
    public Integer getFirstItem() {
        return pageIndex - 1;
    }
}

然后通过page对象来进行对之前分页拦截器的优化

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PageHelperInterceptor1 extends MyMybatisInterceptorAdapter {

    private static final Logger log = LoggerFactory.getLogger(PageHelperInterceptor1.class);

    private String queryMethodPrefix;

    private String queryMethodSuffix;


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (log.isInfoEnabled())
            log.info("----pageHelperInterceptor------");
        //获得sql语句 拼接字符串 limit
        MetaObject metaObject = SystemMetaObject.forObject(invocation);

        String sql = (String) metaObject.getValue("target.delegate.boundSql.sql");
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("target.delegate.mappedStatement");
        String id = mappedStatement.getId();

        if (id.indexOf(queryMethodPrefix) != -1 && id.endsWith(queryMethodSuffix)) {
            //分页相关的操作封装 对象(vo dto)
            //获得Page对象 并设置Page对象 totalSize属性 算出总页数

            //假设 Page
            Page page = new Page(1);

            //select id,name from t_user 获得 全表有多少条数据
            // select count(*) from t_user

            //select id,name from t_user where name = ?;
            //select count(*)fromt t_user where name = ?

            //select id,name from t_user where  name = ? and id = ?;

            String countSql = "select count(*) " + sql.substring(sql.indexOf("from"));
            //JDBC操作
            //1 Connection  PreapredStatement
            Connection conn = (Connection) invocation.getArgs()[0];
            PreparedStatement preparedStatement = conn.prepareStatement(countSql);

           /* preparedStatement.setString(1,?)
            preparedStatement.setString(2,?);*/
            ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("target.delegate.parameterHandler");
            parameterHandler.setParameters(preparedStatement);

            ResultSet resultSet = preparedStatement.executeQuery();
            if(resultSet.next()){
               page.setTotalSize(resultSet.getInt(1));
            }
                String newSql = sql + " limit "+page.getFirstItem()+","+page.getPageCount();

            metaObject.setValue("target.delegate.boundSql.sql", newSql);
        }
        return invocation.proceed();
    }

    @Override
    public void setProperties(Properties properties) {
        this.queryMethodPrefix = properties.getProperty("queryMethodPrefix");
        this.queryMethodSuffix = properties.getProperty("queryMethodSuffix");
    }
}

可以发现这里通过page对象模拟前端请求的DTO参数,通过截取字符串通过拦截器参数使用JDBC查询数据总条数,然后再拼接sql完成分页查询.

继续分析,这里我们是模拟前端new了一个Page对象来作为参数.那么真实的情况应该是这样

mybatis拦截器源码分析_第3张图片

可以看到Page对象从Controller传递到Service最后到Dao,在这个过程中我们在拦截器并无法获取到Page对象.

解决方案:

​ 将Page对象作为dao方法的参数进行传递,那么在拦截器中可以通过invocation参数获取其中的parameterHandler拿到对应的Page对象.

mybatis拦截器源码分析_第4张图片

//直接通过DAO方法的参数 获得Page对象
 Page page = (Page) metaObject.getValue("target.delegate.parameterHandler.parameterObject");

​ 通过将Page对象存入本地线程中,可以保证线程安全性,也可以进行多个线程的并发处理.通过请求过滤器将Page对象参数通过本地线程set进去,然后再拦截器处理时get即可.

import com.baizhiedu.util.Page;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class PageFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        String pageIndexString = req.getParameter("pageIndex");
        int pageIndex = Integer.parseInt(pageIndexString);
        Page page = new Page(pageIndex);

        //Tl.set(page)

        chain.doFilter(request,response);//--- DispatcherServlet ---- Controller --- Service ----DAO
    }

    @Override
    public void destroy() {

    }
}

创建本地线程工具类进行线程中Page对象的管理,提供get和set方法进行操作

public class ThreadLocalUtils {
    private static final ThreadLocal<Page> tl = new ThreadLocal<>();

    public static void set(Page page) {
        tl.set(page);
    }

    public static Page get() {
        return tl.get();
    }
}

将之前通过invocation参数获取的代码改为

Page page = ThreadLocalUtils.get();

最后,记得处理完分页后将线程中的Page对象remove避免出现不需要分页的查询也进行了查询操作影响数据当然最终分页插件没有做到其他数据库的匹配可以通过不同类型的数据库对sql语句拼接limit那块代码进行优化,这里就不在赘述.至此mybatis分页插件完结

sql动态处理工具

使用场景: 当我们准备使用Mybatis拦截器对sql语句进行处理时,可以通过jsqlparser这个工具进行处理.

这里为了简单,就不结合拦截器来进行编码直接使用测试类来展示jsqlparser的使用方法和功能

首先引入jsqlparser依赖

		<dependency>
            <groupId>com.github.jsqlparsergroupId>
            <artifactId>jsqlparserartifactId>
            <version>3.1version>
        dependency>

查询语句的处理

  @Test
    public void testSQLParser() throws JSQLParserException {
        //前俩行代码属于固定语法
        CCJSqlParserManager parserManager = new CCJSqlParserManager();
        //这里只需要把需要进行处理的sql语句字符串作为参数传入new StringReader()中,注意返回类型不要写错查询就返回Selcet对象(Select对象是jsqlparser依赖中的!)
        Select select = (Select) parserManager.parse(new StringReader("select id,name from t_user where name = 'suns' "));
		//下面分别获取了sql语句的表名,where条件和需要查询的字段名
        PlainSelect selectBody = (PlainSelect) select.getSelectBody();

        //FromItem table = selectBody.getFromItem();
        //System.out.println("table = " + table);

     /*   Expression where = selectBody.getWhere();
        System.out.println("where = " + where);*/

       /* List selectItems = selectBody.getSelectItems();
        for (SelectItem selectItem : selectItems) {
            System.out.println("selectItem = " + selectItem);
        }*/
    }

修改语句的处理

@Test
    public void testSQLParser1() throws JSQLParserException {
        CCJSqlParserManager parserManager = new CCJSqlParserManager();
        Update update = (Update) parserManager.parse(new StringReader("update t_user set name='suns',password='12345' where id=1 "));

        /*Table table = update.getTable();
        System.out.println("table = " + table);*/

        //这里是获取需要修改的字段(注意这里获取不了修改的值)
        List<Column> columns = update.getColumns();
        for (Column column : columns) {
            System.out.println(column);
        }
		//这里可以获取到修改到的值,这俩个内容是分开进行获取的
        List<Expression> expressions = update.getExpressions();
        for (Expression expression : expressions) {
            System.out.println(expression);
        }
    }

可以通过与Mybatis拦截器相结合进行Sql语句的动态处理

乐观锁

场景: 当多个请求(线程)并发(同一时间)访问了数据库中的相同的数据,如何保证数据安全.

悲观锁: 数据库底层提供的锁,引入悲观锁保证数据并发访问的安全.将一个并行的操作串行化,等待第一个操作完数据后第二基于第一个操作的结果进行操作,只要执行了增删改操作数据库就会为数据添加悲观锁 也称为行锁.

乐观锁: 应用锁 不涉及到数据库底层真的为数据加锁,并发效率高,安全性低.

实现原理: 版本号的比对(每一次 更新数据的时候
先要 进行版本的比对
如果版本一致 则说明没有其他事物对数据进行操作
如果版本不一致 则说明有些其他事物操作了数据 产生了并发)

如何封装乐观锁

1.保证version列初始值为0,插入操作时 sql insert vers = 0

2.每次更新的过程中与表中vers对比,获取对象version属性值,查询数据库当前这条数据的vers的值

3.如何值一致,进行更新操作并且vers+1

4.如果值不一致,抛出乐观锁异常

import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.StringReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.List;
import java.util.Properties;

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class LockInterceptor extends MyMybatisInterceptorAdapter {

    private static final Logger log = LoggerFactory.getLogger(LockInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (log.isInfoEnabled())
            log.info("----LockInterceptor------");

        MetaObject metaObject = SystemMetaObject.forObject(invocation);
		//获取sql语句
        String sql = (String) metaObject.getValue("target.delegate.boundSql.sql");
        MappedStatement mappedStatement = (MappedStatement) 		metaObject.getValue("target.delegate.mappedStatement");
        //获取dao方法名如 save,selectOne等
        String id = mappedStatement.getId();

        /*
            在用户进行插入操作时,需要由拦截器 设置vers值0
             用户书写的Sql语句:insert into t_user (name) values (#{name});
               封装需要干的事    insert into t_user (name,vers) values (#{name},0)

               问题:如何获得 用户书写SQL ?
               解答:String sql = (String) metaObject.getValue("target.delegate.boundSql.sql");

               问题:如何修改sql语句 为其添加vers 值0 ?
               解决:涉及到对原有sql语句操作,JsqlParser

         */
        //如果是插入操作
        if (id.indexOf("save") != -1) {
            //解析sql语句
            CCJSqlParserManager parserManager = new CCJSqlParserManager();
            Insert insert = (Insert) parserManager.parse(new StringReader(sql));
            //插入的列 vers  匹配对应的值 0
            //列名字 Columns
            List<Column> columns = insert.getColumns();
            columns.add(new Column("vers"));

            //列的值
            ExpressionList itemsList = (ExpressionList) insert.getItemsList();
            List<Expression> expressions = itemsList.getExpressions();
            expressions.add(new LongValue(0));
            insert.setSetExpressionList(expressions);

            //修改完成sql语句后 新的sql语句 交给Mybatis ---> 继续进行?替换
            metaObject.setValue("target.delegate.boundSql.sql", insert.toString());


        }

         /*
             update t_user set name =?,vers = vers+1 where id = ?
             如果进行update操作:
                1. 在提交update操作时,需要对比此时 对象中的version里面存储的值与数据库中vers字段中的值是否相等
                 1.1 如果不等
                       说明已经有其他用户进行了更新 (存在并发) 抛出异常
                 1.2 如果相等
                       可以进行更新操作,并把对应的vers+1

          */

        if (id.indexOf("update") != -1) {

            CCJSqlParserManager parserManager = new CCJSqlParserManager();
            Update update = (Update) parserManager.parse(new StringReader(sql));
            Table table = update.getTable();
            String tableName = table.getName();

            //id值 一定是更新操作中 User id属性存储

            Integer objectId = (Integer) metaObject.getValue("target.delegate.parameterHandler.parameterObject.id");
            Integer version = (Integer) metaObject.getValue("target.delegate.parameterHandler.parameterObject.version");

            Connection conn = (Connection) invocation.getArgs()[0];
            String selectSql = "select vers from " + tableName + " where id = ?";
            PreparedStatement preparedStatement = conn.prepareStatement(selectSql);
            preparedStatement.setInt(1, objectId);
            ResultSet resultSet = preparedStatement.executeQuery();
            int vers = 0;
            if (resultSet.next()) {
                vers = resultSet.getInt(1);
            }

            System.out.println();

            if (version.intValue() != vers) {
                throw new RuntimeException("版本不一致");
            } else {
                //vers+1
                //正常进行数据库更新
                List<Column> columns = update.getColumns();
                columns.add(new Column("vers"));

                List<Expression> expressions = update.getExpressions();
                expressions.add(new LongValue(vers + 1));
                update.setExpressions(expressions);

                metaObject.setValue("target.delegate.boundSql.sql", update.toString());
            }


        }
        return invocation.proceed();
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

至此,mybatis拦截器完结

你可能感兴趣的:(手撕源码,mybatis)