MyBatis源码篇:mybatis拦截器源码分析

本文涉及的产品
可观测可视化 Grafana 版,10个用户账号 1个月
应用实时监控服务-用户体验监控,每月100OCU免费额度
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
简介: MyBatis源码篇: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


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对象,我们进入源码看看


通过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对象来作为参数.那么真实的情况应该是这样


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

解决方案:

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


//直接通过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.jsqlparser</groupId>
            <artifactId>jsqlparser</artifactId>
            <version>3.1</version>
        </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<SelectItem> 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) {
    }
}
相关文章
|
3月前
|
SQL XML Java
mybatis-源码深入分析(一)
mybatis-源码深入分析(一)
|
3月前
|
SQL Java 数据库连接
解决mybatis-plus 拦截器不生效--分页插件不生效
本文介绍了在使用 Mybatis-Plus 进行分页查询时遇到的问题及解决方法。依赖包包括 `mybatis-plus-boot-starter`、`mybatis-plus-extension` 等,并给出了正确的分页配置和代码示例。当分页功能失效时,需将 Mybatis-Plus 版本改为 3.5.5 并正确配置拦截器。
979 6
解决mybatis-plus 拦截器不生效--分页插件不生效
|
2月前
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
76 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
2月前
|
Java 数据库连接 mybatis
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
该文档详细介绍了如何在Springboot Web项目中整合Mybatis,包括添加依赖、使用`@MapperScan`注解配置包扫描路径等步骤。若未使用`@MapperScan`,系统会自动扫描加了`@Mapper`注解的接口;若使用了`@MapperScan`,则按指定路径扫描。文档还深入分析了相关源码,解释了不同情况下的扫描逻辑与优先级,帮助理解Mybatis在Springboot项目中的自动配置机制。
163 0
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
|
4月前
|
XML Java 数据库连接
mybatis源码研究、搭建mybatis源码运行的环境
这篇文章详细介绍了如何搭建MyBatis源码运行的环境,包括创建Maven项目、导入源码、添加代码、Debug运行研究源码,并提供了解决常见问题的方法和链接到搭建好的环境。
mybatis源码研究、搭建mybatis源码运行的环境
|
4月前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。
|
4月前
|
供应链 前端开发 Java
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
该博客文章介绍了一个使用Mybatis、Layui、MVC和JSP技术栈开发的服装库存管理系统,包括注册登录、权限管理、用户和货号管理、库存管理等功能,并提供了源码下载链接。
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
|
2月前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
145 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
|
2月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
76 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
2月前
|
前端开发 Java Apache
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
本文详细讲解了如何整合Apache Shiro与Spring Boot项目,包括数据库准备、项目配置、实体类、Mapper、Service、Controller的创建和配置,以及Shiro的配置和使用。
511 1
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
下一篇
DataWorks