Springboot管理系统数据权限过滤(二)——SQL拦截器

上一节Springboot管理系统数据权限过滤——ruoyi实现方案对数据权限实现方案有了认识,本文将进一步优化权限过滤方案,实现对业务代码零入侵。

回顾上一章中权限方案:

  • 主要是通过注解拦截,拼接好权限脚本后,放到对象变量里面,然后在SQL中拼接该变量;使业务代码被入侵了。

为了实现对业务零入侵,实则是在SQL编写的时候,希望通过框架实现权限脚本的自动拼接,而非人为添加。
本文权限控制需要达到的效果:

  • 1.还是对组织进行权限控制;
  • 2.去掉编写sql时拼接权限过滤参数;使权限代码0侵入;

步骤:

1. 搭建springboot框架,完成mybatisplus集成和swagger集成

pom.xml文件


<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.3.RELEASE</version><relativePath/></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 方便等会写单元测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- 实现对数据库连接池的自动化配置 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency> <!-- 本示例,我们使用 MySQL --><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.48</version></dependency><!-- 实现对 MyBatis 的自动化配置 --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.1</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.2</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><!-- 引入 Swagger 依赖 --><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><!-- 引入 Swagger UI 依赖,以实现 API 接口的 UI 界面 --><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency></dependencies>

application.yaml

spring:# datasource 数据源配置内容datasource:url: jdbc:mysql://127.0.0.1:3306/test_users?useSSL=false&useUnicode=true&characterEncoding=UTF-8driver-class-name: com.mysql.jdbc.Driverusername: rootpassword: 123456# mybatis-plus 配置内容
mybatis-plus:configuration:map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。global-config:db-config:id-type: auto # ID 主键自增logic-delete-value: 1 # 逻辑已删除值(默认为 1)logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)mapper-locations: classpath*:mapper/*.xmltype-aliases-package: com.luo.chengrui.labs.lab02.dataobject # 配置数据库实体包路径# logging
logging:level:# dao 开启 debug 模式 mybatis 输入 sqlcom:luo:chengrui:labs: debug

UserDao.java

package com.luo.chengrui.labs.lab02.dataobject;import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;import java.util.Date;/*** @author* @version 1.0.0* @description* @createTime 2023/07/20*/
@Data
@Accessors(chain = true)
@TableName(value = "users")
public class UserDO {/*** 用户编号*/private Long id;/*** 账号*/private String username;/*** 密码(明文)* <p>* ps:生产环境下,千万不要明文噢*/private String password;/*** 创建时间*/private Date createTime;
}

UserMapper.java

package com.luo.chengrui.labs.lab02.mapper;import com.luo.chengrui.labs.lab02.dataobject.UserDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
public interface UserMapper {UserDO selectById(@Param("id") Integer id);List<UserDO> selectList();
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luo.chengrui.labs.lab02.mapper.UserMapper"><sql id="FIELDS">id, username</sql><select id="selectById" parameterType="Integer" resultType="UserDO">SELECT<include refid="FIELDS"/>FROM usersWHERE id = #{id}</select><select id="selectList" resultType="UserDo">SELECT<include refid="FIELDS"/>FROM users</select></mapper>

UserService.java

package com.luo.chengrui.labs.lab02.service;import com.luo.chengrui.labs.lab02.annotation.DataScope;
import com.luo.chengrui.labs.lab02.dataobject.UserDO;
import com.luo.chengrui.labs.lab02.mapper.UserMapper;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;/*** @author* @version 1.0.0* @description* @createTime 2023/07/21*/
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;private UserService self() {return (UserService) AopContext.currentProxy();}/*** 方法未使用 @Transactional 注解,不会开启事务。* 对于 OrderMapper 和 UserMapper 的查询操作,分别使用其接口上的 @DS 注解,找到对应的数据源,执行操作。* 这样一看,在未开启事务的情况下,我们已经能够自由的使用多数据源落。*/public void method() {// 查询订单UserDO user = userMapper.selectById(1);System.out.println(user);}@DataScopepublic void method01() {// 查询订单UserDO user = userMapper.selectById(1);System.out.println(user);}@DataScopepublic List<UserDO> selectList() {return userMapper.selectList();}
}

UserController.java

package com.luo.chengrui.labs.lab02.controller;import com.luo.chengrui.labs.lab02.dataobject.UserDO;
import com.luo.chengrui.labs.lab02.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;/*** @author* @version 1.0.0* @description* @createTime 2023/07/17*/
@RestController
@RequestMapping("/users")
@Api(tags = "用户 API 接口")
public class UserController {@AutowiredUserService userService;@GetMapping("/list")@ApiOperation(value = "查询用户列表", notes = "目前仅仅是作为测试,所以返回用户全列表")public List<UserDO> list() {// 查询列表List<UserDO> result = userService.selectList();// 返回列表return result;}}

SwaggerConfiguration.java

package com.luo.chengrui.labs.lab02.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;/*** 访问地址:/swagger-ui.html* @author* @version 1.0.0* @description* @createTime 2023/07/17*/
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {@Beanpublic Docket createRestApi() {// 创建 Docket 对象return new Docket(DocumentationType.SWAGGER_2) // 文档类型,使用 Swagger2.apiInfo(this.apiInfo()) // 设置 API 信息// 扫描 Controller 包路径,获得 API 接口.select().apis(RequestHandlerSelectors.basePackage("com.luo.chengrui.labs.lab02.controller")).paths(PathSelectors.any())// 构建出 Docket 对象.build();}/*** 创建 API 信息*/private ApiInfo apiInfo() {return new ApiInfoBuilder().title("测试接口文档示例").description("我是一段描述").version("1.0.0") // 版本号.contact(new Contact("芋艿", "http://www.iocoder.cn", "zhijiantianya@gmail.com")) // 联系人.build();}
}

Lab0201Application.java

package com.luo.chengrui.labs.lab02;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** @author* @version 1.0.0* @description* @createTime 2023/07/21*/
@SpringBootApplication
@MapperScan(basePackages = "com.luo.chengrui.labs.lab02.mapper")
public class Lab0201Application {public static void main(String[] args) {SpringApplication.run(Lab0201Application.class, args);}
}

到此完成框架搭建,访问:http://localhost:8080/swagger-ui.html,可看到以下页面即为成功。

swagger 接口文档

2. 配置sql拦截器

创建 DataPermissionDatabaseInterceptor.java 类,类中大部分代码是对sql的解析,对表名的解析,对where语句的解析,真正需要关注的逻辑只是少部分,本部分代码里暂未添加对权限的控制,在下一文章中添加。
该类继承JsqlParserSupport ,同时实现InnerInterceptor接口

  • JsqlParserSupport 用于解析sql语句,可以对sql进行改造;方法有:processSelect、processUpdate、processDelete等;
  • InnerInterceptor 在执行sql语句之前的拦截器,在JspParserSuport方法执行之前执行 。方法有:beforeQuery、beforeUpdate、beforePrepare等。
    通过实现以上5个方法即可对sql进行改造。
package com.luo.chengrui.labs.lab02.datapermission;import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;import java.sql.Connection;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;/*** 数据权限拦截器,通过 {@link } 数据权限规则,重写 SQL 的方式来实现* 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, List)} 方法* 主要是在执行SQL前拦截器,在执行之前可重写SQL** @author 芋道源码*/
@RequiredArgsConstructor
public class DataPermissionDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor {private static final String MYSQL_ESCAPE_CHARACTER = "`";@Override // SELECT 场景public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);try {// 初始化上下文// 处理 SQLmpBs.sql(parserSingle(mpBs.sql(), null));} finally {}}@Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景(因为 INSERT 不需要数据权限)public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);MappedStatement ms = mpSh.mappedStatement();SqlCommandType sct = ms.getSqlCommandType();if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {// 获得 Mapper 对应的数据权限的规则PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();try {// 初始化上下文// 处理 SQLmpBs.sql(parserMulti(mpBs.sql(), null));} finally {}}}@Overrideprotected void processSelect(Select select, int index, String sql, Object obj) {processSelectBody(select.getSelectBody());List<WithItem> withItemsList = select.getWithItemsList();if (!CollectionUtils.isEmpty(withItemsList)) {withItemsList.forEach(this::processSelectBody);}}/*** update 语句处理*/@Overrideprotected void processUpdate(Update update, int index, String sql, Object obj) {final Table table = update.getTable();update.setWhere(this.builderExpression(update.getWhere(), table));}/*** delete 语句处理*/@Overrideprotected void processDelete(Delete delete, int index, String sql, Object obj) {delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable()));}// ========== 和 TenantLineInnerInterceptor 一致的逻辑 ==========protected void processSelectBody(SelectBody selectBody) {if (selectBody == null) {return;}if (selectBody instanceof PlainSelect) {processPlainSelect((PlainSelect) selectBody);} else if (selectBody instanceof WithItem) {WithItem withItem = (WithItem) selectBody;processSelectBody(withItem.getSubSelect().getSelectBody());} else {SetOperationList operationList = (SetOperationList) selectBody;List<SelectBody> selectBodyList = operationList.getSelects();if (CollectionUtils.isNotEmpty(selectBodyList)) {selectBodyList.forEach(this::processSelectBody);}}}/*** 处理 PlainSelect*/protected void processPlainSelect(PlainSelect plainSelect) {//#3087 githubList<SelectItem> selectItems = plainSelect.getSelectItems();if (CollectionUtils.isNotEmpty(selectItems)) {selectItems.forEach(this::processSelectItem);}// 处理 where 中的子查询Expression where = plainSelect.getWhere();processWhereSubSelect(where);// 处理 fromItemFromItem fromItem = plainSelect.getFromItem();List<Table> list = processFromItem(fromItem);List<Table> mainTables = new ArrayList<>(list);// 处理 joinList<Join> joins = plainSelect.getJoins();if (CollectionUtils.isNotEmpty(joins)) {mainTables = processJoins(mainTables, joins);}// 当有 mainTable 时,进行 where 条件追加if (CollectionUtils.isNotEmpty(mainTables)) {plainSelect.setWhere(builderExpression(where, mainTables));}}private List<Table> processFromItem(FromItem fromItem) {// 处理括号括起来的表达式while (fromItem instanceof ParenthesisFromItem) {fromItem = ((ParenthesisFromItem) fromItem).getFromItem();}List<Table> mainTables = new ArrayList<>();// 无 join 时的处理逻辑if (fromItem instanceof Table) {Table fromTable = (Table) fromItem;mainTables.add(fromTable);} else if (fromItem instanceof SubJoin) {// SubJoin 类型则还需要添加上 where 条件List<Table> tables = processSubJoin((SubJoin) fromItem);mainTables.addAll(tables);} else {// 处理下 fromItemprocessOtherFromItem(fromItem);}return mainTables;}/*** 处理where条件内的子查询* <p>* 支持如下:* 1. in* 2. =* 3. >* 4. <* 5. >=* 6. <=* 7. <>* 8. EXISTS* 9. NOT EXISTS* <p>* 前提条件:* 1. 子查询必须放在小括号中* 2. 子查询一般放在比较操作符的右边** @param where where 条件*/protected void processWhereSubSelect(Expression where) {if (where == null) {return;}if (where instanceof FromItem) {processOtherFromItem((FromItem) where);return;}if (where.toString().indexOf("SELECT") > 0) {// 有子查询if (where instanceof BinaryExpression) {// 比较符号 , and , or , 等等BinaryExpression expression = (BinaryExpression) where;processWhereSubSelect(expression.getLeftExpression());processWhereSubSelect(expression.getRightExpression());} else if (where instanceof InExpression) {// inInExpression expression = (InExpression) where;Expression inExpression = expression.getRightExpression();if (inExpression instanceof SubSelect) {processSelectBody(((SubSelect) inExpression).getSelectBody());}} else if (where instanceof ExistsExpression) {// existsExistsExpression expression = (ExistsExpression) where;processWhereSubSelect(expression.getRightExpression());} else if (where instanceof NotExpression) {// not existsNotExpression expression = (NotExpression) where;processWhereSubSelect(expression.getExpression());} else if (where instanceof Parenthesis) {Parenthesis expression = (Parenthesis) where;processWhereSubSelect(expression.getExpression());}}}protected void processSelectItem(SelectItem selectItem) {if (selectItem instanceof SelectExpressionItem) {SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;if (selectExpressionItem.getExpression() instanceof SubSelect) {processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody());} else if (selectExpressionItem.getExpression() instanceof Function) {processFunction((Function) selectExpressionItem.getExpression());}}}/*** 处理函数* <p>支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)<p>* <p> fixed gitee pulls/141</p>** @param function*/protected void processFunction(Function function) {ExpressionList parameters = function.getParameters();if (parameters != null) {parameters.getExpressions().forEach(expression -> {if (expression instanceof SubSelect) {processSelectBody(((SubSelect) expression).getSelectBody());} else if (expression instanceof Function) {processFunction((Function) expression);}});}}/*** 处理子查询等*/protected void processOtherFromItem(FromItem fromItem) {// 去除括号while (fromItem instanceof ParenthesisFromItem) {fromItem = ((ParenthesisFromItem) fromItem).getFromItem();}if (fromItem instanceof SubSelect) {SubSelect subSelect = (SubSelect) fromItem;if (subSelect.getSelectBody() != null) {processSelectBody(subSelect.getSelectBody());}} else if (fromItem instanceof ValuesList) {logger.debug("Perform a subQuery, if you do not give us feedback");} else if (fromItem instanceof LateralSubSelect) {LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;if (lateralSubSelect.getSubSelect() != null) {SubSelect subSelect = lateralSubSelect.getSubSelect();if (subSelect.getSelectBody() != null) {processSelectBody(subSelect.getSelectBody());}}}}/*** 处理 sub join** @param subJoin subJoin* @return Table subJoin 中的主表*/private List<Table> processSubJoin(SubJoin subJoin) {List<Table> mainTables = new ArrayList<>();if (subJoin.getJoinList() != null) {List<Table> list = processFromItem(subJoin.getLeft());mainTables.addAll(list);mainTables = processJoins(mainTables, subJoin.getJoinList());}return mainTables;}/*** 处理 joins** @param mainTables 可以为 null* @param joins      join 集合* @return List<Table> 右连接查询的 Table 列表*/private List<Table> processJoins(List<Table> mainTables, List<Join> joins) {// join 表达式中最终的主表Table mainTable = null;// 当前 join 的左表Table leftTable = null;if (mainTables == null) {mainTables = new ArrayList<>();} else if (mainTables.size() == 1) {mainTable = mainTables.get(0);leftTable = mainTable;}//对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名Deque<List<Table>> onTableDeque = new LinkedList<>();for (Join join : joins) {// 处理 on 表达式FromItem joinItem = join.getRightItem();// 获取当前 join 的表,subJoint 可以看作是一张表List<Table> joinTables = null;if (joinItem instanceof Table) {joinTables = new ArrayList<>();joinTables.add((Table) joinItem);} else if (joinItem instanceof SubJoin) {joinTables = processSubJoin((SubJoin) joinItem);}if (joinTables != null) {// 如果是隐式内连接if (join.isSimple()) {mainTables.addAll(joinTables);continue;}// 当前表是否忽略Table joinTable = joinTables.get(0);List<Table> onTables = null;// 如果不要忽略,且是右连接,则记录下当前表if (join.isRight()) {mainTable = joinTable;if (leftTable != null) {onTables = Collections.singletonList(leftTable);}} else if (join.isLeft()) {onTables = Collections.singletonList(joinTable);} else if (join.isInner()) {if (mainTable == null) {onTables = Collections.singletonList(joinTable);} else {onTables = Arrays.asList(mainTable, joinTable);}mainTable = null;}mainTables = new ArrayList<>();if (mainTable != null) {mainTables.add(mainTable);}// 获取 join 尾缀的 on 表达式列表Collection<Expression> originOnExpressions = join.getOnExpressions();// 正常 join on 表达式只有一个,立刻处理if (originOnExpressions.size() == 1 && onTables != null) {List<Expression> onExpressions = new LinkedList<>();onExpressions.add(builderExpression(originOnExpressions.iterator().next(), onTables));join.setOnExpressions(onExpressions);leftTable = joinTable;continue;}// 表名压栈,忽略的表压入 null,以便后续不处理onTableDeque.push(onTables);// 尾缀多个 on 表达式的时候统一处理if (originOnExpressions.size() > 1) {Collection<Expression> onExpressions = new LinkedList<>();for (Expression originOnExpression : originOnExpressions) {List<Table> currentTableList = onTableDeque.poll();if (CollectionUtils.isEmpty(currentTableList)) {onExpressions.add(originOnExpression);} else {onExpressions.add(builderExpression(originOnExpression, currentTableList));}}join.setOnExpressions(onExpressions);}leftTable = joinTable;} else {processOtherFromItem(joinItem);leftTable = null;}}return mainTables;}/*** 处理条件** @param currentExpression 当前 where 条件* @param table             单个表*/protected Expression builderExpression(Expression currentExpression, Table table) {return this.builderExpression(currentExpression, Collections.singletonList(table));}/*** 处理条件** @param currentExpression 当前 where 条件* @param tables            多个表*/protected Expression builderExpression(Expression currentExpression, List<Table> tables) {// 没有表需要处理直接返回if (CollectionUtils.isEmpty(tables)) {return currentExpression;}// 第一步,获得 Table 对应的数据权限条件Expression dataPermissionExpression = null;for (Table table : tables) {// 构建每个表的权限 Expression 条件Expression expression = buildDataPermissionExpression(table);if (expression == null) {continue;}// 合并到 dataPermissionExpression 中dataPermissionExpression = dataPermissionExpression == null ? expression: new AndExpression(dataPermissionExpression, expression);}// 第二步,合并多个 Expression 条件if (dataPermissionExpression == null) {return currentExpression;}if (currentExpression == null) {return dataPermissionExpression;}// ① 如果表达式为 Or,则需要 (currentExpression) AND dataPermissionExpressionif (currentExpression instanceof OrExpression) {return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression);}// ② 如果表达式为 And,则直接返回 where AND dataPermissionExpressionreturn new AndExpression(currentExpression, dataPermissionExpression);}/*** 构建指定表的数据权限的 Expression 过滤条件** @param table 表* @return Expression 过滤条件*/private Expression buildDataPermissionExpression(Table table) {// 生成条件Expression allExpression = null;return allExpression;}}

拦截器定义好,下一步是将拦截器放置到mybatis的拦截器队列中。
定义一个公共的Configuration类:
DataPermissionConfiguration.java

@Configuration
public class DataPermissionConfiguration {/** 将自定义拦截器,添加到mybatis拦截器队列中。让拦截器生效*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(List<DataPermissionRule> dataPermissionRule) {MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();// 分页插件
//        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());//添加权限拦截器。DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor();List<InnerInterceptor> inners = new ArrayList<>(mybatisPlusInterceptor.getInterceptors());inners.add(0, inner);mybatisPlusInterceptor.setInterceptors(inners);return mybatisPlusInterceptor;}}

完成上面配置后,每执行一个SQL都会被我们定义的拦截器。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/222929.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

c题目17:写一个swap函数,可以交换2个整数变量的值。(分别用普通方式和指针方式实现,对比结果)

每日小语 我坐着&#xff0c;观望世界上所有的忧患&#xff0c;所有的压迫和耻辱看着&#xff0c;听着&#xff0c;一声不响。——惠特曼 自己思考 最近这段时间新的感悟似乎也没有&#xff0c;但我发现我和别人的思想越来越不同&#xff0c;只能跟极少数人产生共鸣&#xff0…

DevOps 和人工智能 – 天作之合

如今&#xff0c;人工智能和机器学习无处不在&#xff0c;所以它们开始在 DevOps 领域崭露头角也毫不令人意外。人工智能和机器学习正在通过自动化任务改变 DevOps&#xff0c;并使各企业的软件开发生命周期更高效、更深刻和更安全。我们在 DevOps 趋势中简要讨论过这一问题&am…

使用shell脚本给日志文件瘦身

一、前言 后台系统运行久了&#xff0c;日志文件的体积日渐增多&#xff0c;除了使用常用的日志框架如logback对日志进行按天打印、按大小分割等方式外&#xff0c;还可以使用shell命令来对大日志进行瘦身。 本篇使用sed指令来对文件进行操作&#xff0c;具体操作如下&#xf…

实现进程间的通信

本例程是开发一款能实现进程通信的DLL。本例程以Visual Studio 2015为例。在Visual Studio 2013&#xff0c;Visual Studio 2017都是可以。 第一步&#xff1a;在Visual Studio 2015中&#xff0c;创建DLL工程。如何创建DL&#xff0c;在这里就不作具体说明了。百度都有许多创建…

国际语音群呼系统有哪些应用场景?

国际语音群呼可应用于广告营销、消息通知、客情维护、金融催收等场景&#xff0c;助力出海企业产品营销和品牌推广。 广告营销 出海企业可以通过国际语音群呼系统&#xff0c;向目标市场的潜在客户进行广告宣传。例如&#xff0c;企业可以在系统中录制有关产品的宣传语&#…

发展模式 Fortran 错误记录2023-12-15

/data/chengxl/CAS-ESM2.0-test1/models/atm/iap/src/physics/pbl_iap.F90(476): error #6236: A specification statement cannot appear in the executable section. real(r8) :: rrho(pcols) ! 1/rho m^3/kg -----^ 定义语句不能出现在可执行部分。 我忘记把临时写的定义…

外汇天眼:Coinbase国际交易所将启动现货市场

Coinbase宣布了Coinbase国际交易所扩张的下一阶段——退出符合条件客户的非美国现货市场。 这一最新发展旨在满足Coinbase全球用户群体的独特需求和需求&#xff0c;同时强化其扩大国际访问可信产品和服务的战略使命。 Coinbase国际交易所现货交易的推出和扩展将分阶段进行。1…

Java数据类型相关

数据类型 Java有哪些数据类型 定义&#xff1a;Java语言是强类型语言&#xff0c;对于每一种数据都定义了明确的具体的数据类 型&#xff0c;在内存中分配了不同大小的内存空间。 分类&#xff1a; 基本数据类型 数值型 整数类型(byte,short,int,long) 浮点类型(float,dou…

【数据结构】模式匹配之KMP算法与Bug日志—C/C++实现

​&#x1f308;个人主页&#xff1a;Sarapines Programmer&#x1f525; 系列专栏&#xff1a;《数据结构奇遇记》&#x1f516;墨香寄清辞&#xff1a;墨痕寄壮志&#xff0c;星辰梦未满。 通幽径心凝意&#xff0c;剑指苍穹势如山。 目录 &#x1f31e;1. 模式匹配的基本概念…

Spring Boot之自定义starter

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Spring Boot的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一. starter是什么 二.为什么要使…

计算机操作系统-第十六天

目录 线程的实现方式 用户级线程 内核级线程 多线程模型 一对一模型 多对多模型 多对多模型 本节思维导图 线程的实现方式 用户级线程 历史背景&#xff1a;早期操作系统只支持进程&#xff0c;不支持线程&#xff0c;当时的线程是由线程库实现的 本质&#xff1a;从…

【普中】基于51单片机简易计算器显示设计( proteus仿真+程序+设计报告+实物演示+讲解视频)

目录标题 &#x1f4df;1. 主要功能&#xff1a;&#x1f4df;2. 讲解视频&#xff1a;&#x1f4df;3. 设计说明书(报告)&#x1f4df;4. 仿真&#x1f4df;5. 实物烧录和现象&#x1f4df;6. 程序代码&#x1f4df;7. 设计资料内容清单 【普中开发板】基于51单片机简易计算器…

processon使用及流程图和泳道图的绘画(登录界面流程图,门诊流程图绘制门诊泳道图,住院泳道图,OA会议泳道图),Axure自定义元件

目录 一.processon图形的使用场景介绍 二.流程图绘画 三.泳道图的绘画 1.绘制门诊流程图绘制门诊泳道图 2. 绘制住院泳道图​编辑 3.绘制药库采购入库流程图 4.绘制OA会议泳道图 四.Axure自定义元件 1.Axure载入元件库 一.processon图形的使用场景介绍 二.流程图绘画 示例&…

鸿蒙开发组件之Web

一、加载一个url myWebController: WebviewController new webview.WebviewControllerbuild() {Column() {Web({src: https://www.baidu.com,controller: this.myWebController})}.width(100%).height(100%)} 二、注意点 2.1 不能用Previewer预览 Web这个组件不能使用预览…

《PCL多线程加速处理》-配准-icp

《PCL多线程加速处理》-配准-icp 一、效果展示二、具体实现三、代码一、效果展示 数据越大,速度提升效果越快 1、48万点 2、十万点 3、三万点 4、9000点 配准数据 二、具体实现

构建智能外卖跑腿小程序:技术实践与代码示例

在快节奏的现代生活中&#xff0c;外卖跑腿服务已成为人们日常生活中不可或缺的一部分。为了提供更智能、高效的外卖跑腿体验&#xff0c;本文将深入探讨构建一款智能外卖跑腿小程序所需的关键技术&#xff0c;并提供相应的代码示例。 1. 地理位置服务的整合 外卖跑腿小程序…

小程序 -网络请求post/get

1.1网络请求的概念(post和get) 1.2步骤 1.3 应用函数 js里面写&#xff0c;用bindtap绑在控件上&#xff0c;就不讲了 实例代码&#xff1a; //发起get数据请求get_info(){wx.request({url:https://www.escook.cn/api/get,//请求的接口地址,必须基于https协议//请求的方式met…

SpringBoot项目打成War包部署

简介 一般情况下&#xff0c;在SpringBoot项目开发完成进行服务器部署时&#xff0c;都是打成JAR包进行部署运行的。但是在有些情况下也需要将其打成War包使用Tomcat进行部署。本篇文章就简单介绍一下SpringBoot如何打成War包。 注意&#xff1a; 测试Demo的SpringBoot版本为2…

python selenium chrome114版本之后环境配置和携带缓存打开chrome

尽力局 chrome驱动环境配置chrome打开带缓存设置待缓存打开自动关闭浏览器自动关闭浏览器弹窗 最终代码找资料难啊最终效果代码 依赖包和生成依赖包方法关闭谷歌升级 chrome驱动环境配置 网上找到的资料&#xff0c;我现在安装的是120版本的&#xff0c;这个资料是可行的。比较…

对局域网络中应用了网络变压器 POE供电功能的供电端设备间的连接方法

Hqst华轩盛(石门盈盛)电子导读&#xff1a;一起来了解局域网络中应用了网络变压器 POE供电功能的设备间的来连接方法 POE标准为使用以太网的传输电缆输送直流电到POE兼容的设备定义了两种连接方法: 第一,中间跨接法 一种称作"中间跨接法"( Mid -Span ),使用独立的PoE…