一种灵活的数据权限思路(AOP、反射、MyBatis拦截器)

47f1fc00888b3cc1469528a85dbffa2f.jpeg来源:juejin.cn/post/7267090979537944631

👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡 /  赠书福利

全栈前后端分离博客项目 1.0 版本完结啦,2.0 正在更新中..., 演示链接http://116.62.199.48/ ,全程手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,直到项目上线。目前已更新了189小节,累计33w+字,讲解图:1308张,还在持续爆肝中.. 后续还会上新更多项目,目标是将Java领域典型的项目都整一波,如秒杀系统, 在线商城, IM即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,已有950+小伙伴加入(早鸟价超低)

c226743a65d67facb51131f69efc937d.gif

  • 1 前言

  • 2 需求

  • 3 设计思路

  • 4 例子1 查看订单金额大于100且小于500的订单

    • 规则配置

    • 代码

  • 5 例子2 查看收货人地址模糊查询钦南区的订单

    • 规则配置

    • 代码

  • 6 当然,一键代码生成,一句代码都不用写即可,实现单表的增删改查

    • EntityController

    • EntityService

    • EntityServiceImpl

    • 自定义注解

  • [7 项目地址 wonder-server: 一个有意思的权限管理系统2]

  • 参考资料


1 前言

我一年java,在小公司,当前公司权限这块都没有成熟的方案,目前我知道权限分为功能权限和数据权限,我不知道数据权限这块大家是怎么解决的,但在实际项目中我遇到数据权限真的复杂,你永远不知道业主在这方面的需求是什么。

我也有去搜索在这方面是怎么做,但是我在gitee、github搜到的权限管理系统他们都是这么实现的:查看全部数据自定义数据权限本部门数据权限本部门及以下数据仅本人数据权限,但是这种控制粒度完全不够的,所以就想自己实现一下。

2 需求

需求一 有一个单位企业的树,企业都是挂在某个单位下面的,企业是分类型的(餐饮企业经营企业生产企业),业主需要单位的人限定某些单位只能看一个或他指定的某个类型的企业。现在指定角色A只能查看餐饮经营企业,那就只能使用查看自定义部门数据这个,然后在10000家企业里面慢慢勾选符合的企业,这样可以是可以,但是我觉得这样做不太妥。

估计有人说:那你把三种类型的企业分组,餐饮企业挂在餐饮分组下,其他同理。然后用自定义数据权限选中那两个不就可以了吗? 可以是可以,但是我不是业主,业主要求了那些企业必须挂在哪些单位下,在页面显示的树也不能显示什么餐饮企业分组生产企业... 说到底,除非你有办法改变业主的想法。

需求二 类似订单吧,角色A只能查看未支付的订单,角色B只能看交易金额在100~1000元的订单。

用通用的那5种权限对这两个需求已经是束手无策了。

3 设计思路

后来我看到一篇文章【数据权限就该这么实现(设计篇) [1]】,对我有很大的启发,从数据库字段下手,用规则来处理

b9cd1e2957dd17bc28950798d45b3f46.jpeg

图片

我以这个文章的思路为基础,设计了这么一个关系

666bc695c82f8e93d6ec4d5bd522fcce.jpeg

图片

主要还是这张规则表,通过在页面配置好相关的规则来实现对某个字段的控制

CREATE TABLE `sys_rule` (
  `id` bigint NOT NULL,
  `remark` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '备注',
  `mark_id` bigint DEFAULT NULL,
  `table_alias` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '表别名',
  `column_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '数据库字段名',
  `splice_type` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '拼接类型 SpliceTypeEnum',
  `expression` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '表达式 ExpressionEnum',
  `provide_type` tinyint DEFAULT NULL COMMENT 'ProvideTypeEnum 值提供类型,1-值,2-方法',
  `value1` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '值1',
  `value2` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '值2',
  `class_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '全限定类名',
  `method_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '方法名',
  `formal_param` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '形参,分号隔开',
  `actual_param` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '实参,分号隔开',
  `create_time` datetime DEFAULT NULL,
  `create_by` bigint DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `update_by` bigint DEFAULT NULL,
  `deleted` bit(1) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='规则表';

整体思路就是通过页面来对特定的接口设置规则,如果提供类型是@DataScope注解用在方法上,那么默认机会在执行SQL前去拼接对应的数据权限。

如果提供类型是方法@DataScope注解用在方法上,那么会根据你配置的方法名参数类型去反射执行对应的方法,得到该规则能查看的所有idList,然后在执行SQL前去拼接对应的数据权限,这是默认的处理方式。如果@DataScope注解使用在形参上或者使用Service提供的方法接口,那么需要开发者手动处理,返回什么那么是开发者自定义了。

所以字段你自己定,联表也没问题、反射执行什么方法、参数是什么、过程怎么样也是你自己定,灵活性很高(至少我是这么认为的,哈哈哈哈哈哈)

新建 DataScopeHandler
@Component
public class DataScopeHandler implements DataPermissionHandler {

    Map<String, ExpressStrategy> expressStrategyMap = new HashMap<>();

    @PostConstruct
    public void init() {
        expressStrategyMap.put(ExpressionEnum.EQ.toString(), new EqStrategyImpl());
        expressStrategyMap.put(ExpressionEnum.NE.toString(), new NeStrategyImpl());
        // ....其他情况
    }

    @Override
    public Expression getSqlSegment(Expression oldWhere, String mappedStatementId) {
        DataScopeAspect.DataScopeParam dataScopeParam = DataScopeAspect.getDataScopeParam();
        // 没有规则就不限制
        if (dataScopeParam == null || dataScopeParam.getDataScopeInfo() == null || CollectionUtil.isEmpty(dataScopeParam.getDataScopeInfo().getRuleList()) || SecurityUtil.isAdmin()) {
            return oldWhere;
        }

        Expression newWhere = null;

        DataScopeInfo dataScopeInfo = dataScopeParam.getDataScopeInfo();
        List<RuleDto> ruleList = dataScopeInfo.getRuleList();
        for (RuleDto rule : ruleList) {
            ExpressStrategy expressStrategy = expressStrategyMap.get(rule.getExpression());
            if (expressStrategy == null)
                throw new IllegalArgumentException("错误的表达式:" + rule.getExpression());

            newWhere = expressStrategy.apply(rule, newWhere);
        }

        return oldWhere == null ? newWhere : new AndExpression(oldWhere, new Parenthesis(newWhere));
    }
}
使用策略模式 ExpressStrategy
public interface ExpressStrategy {

    Expression apply(RuleDto rule, Expression where);

    default Object getValue(RuleDto rule) {
        if (rule.getProvideType().equals(ProvideTypeEnum.METHOD.getCode())) {
            return rule.getResult();
        } else if (rule.getProvideType().equals(ProvideTypeEnum.VALUE.getCode())) {
            return rule.getValue1();
        } else {
            throw new IllegalArgumentException("错误的提供类型");
        }
    }

    default Column getColumn(RuleDto rule) {
        String sql = "".equals(rule.getTableAlias()) || rule.getTableAlias() == null ? rule.getColumnName() : rule.getTableAlias() + "." + rule.getColumnName();
        return new Column(sql);
    }

    default boolean isOr(String spliceType) {
        if (!spliceType.equals(SpliceTypeEnum.AND.toString()) && !spliceType.equals(SpliceTypeEnum.OR.toString())) {
            throw new IllegalArgumentException("错误的拼接类型:" + spliceType);
        }
        return spliceType.equals(SpliceTypeEnum.OR.toString());
    }

}
其中一种策略 EqStrategyImpl

这里只列举其中一种情况,我们处理 = 操作

public class EqStrategyImpl implements ExpressStrategy{

    @Override
    public Expression apply(RuleDto rule, Expression where) {
        boolean or = isOr(rule.getSpliceType());
        Column column = getColumn(rule);
        Object value = getValue(rule);
        StringValue valueExpression = new StringValue((String) value);
        EqualsTo equalsTo = new EqualsTo(column, valueExpression);
        if (or) {
            where = where == null ? equalsTo : new OrExpression(where, equalsTo);
        } else {
            where = where == null ? equalsTo : new AndExpression(where, equalsTo);
        }
        return where;
    }
}
注册 DataScopeHandler
@Configuration
public class MyBatisPlusConfig {

    @Autowired
    private DataScopeHandler dataScopeHandler;

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 添加自定义的数据权限处理器
        DataPermissionInterceptor dataPermissionInterceptor = new DataPermissionInterceptor();
        dataPermissionInterceptor.setDataPermissionHandler(dataScopeHandler);
        interceptor.addInnerInterceptor(dataPermissionInterceptor);

        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
自定义注解@DataScope
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
    /**
     * 标记这是哪个接口,
     */
    String value();
}
切面处理
@Aspect
@Slf4j
@Component
public class DataScopeAspect {

    @Autowired
    private MarkService dataScopeService;

    // 通过ThreadLocal记录权限相关的属性值
    public static ThreadLocal<DataScopeParam> threadLocal = new ThreadLocal<>();

    public static DataScopeParam getDataScopeParam() {
        return threadLocal.get();
    }

    // 方法切点
    @Pointcut("@annotation(com.gitee.whzzone.common.annotation.DataScope)")
    public void methodPointCut() {
    }

    @After("methodPointCut()")
    public void clearThreadLocal() {
        threadLocal.remove();
        log.debug("threadLocal.remove()");
    }

    @Before("methodPointCut()")
    public void doBefore(JoinPoint point) {

        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        // 获得注解
        DataScope dataScope = method.getAnnotation(DataScope.class);

        try {
            if (dataScope != null && !SecurityUtil.isAdmin()) {
                // 拿到注解的值
                String scopeName = dataScope.value();
                
                // 根据注解的值去解析
                DataScopeInfo dataScopeInfo = dataScopeService.execRuleByName(scopeName);

                DataScopeParam dataScopeParam = new DataScopeParam();

                dataScopeParam.setDataScopeInfo(dataScopeInfo);

                threadLocal.set(dataScopeParam);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("数据权限 method 切面错误:" + e.getMessage());
        }

    }

    @Data
    public static class DataScopeParam {
        private DataScopeInfo dataScopeInfo;
    }

}
解析

根据注解的值,能拿到一个mark,根据这个标记可以查询到对应的rules,则可以开始进行解析

private DataScopeInfo execRuleHandler(List<Rule> rules) {
    if (CollectionUtil.isEmpty(rules))
        return null;

    List<RuleDto> ruleList = new ArrayList<>();

    for (Rule rule : rules) {
        RuleDto dto = new RuleDto();
        BeanUtil.copyProperties(rule, dto);

        if (rule.getProvideType().equals(ProvideTypeEnum.VALUE.getCode())) {
            ruleList.add(dto);

        } else if (rule.getProvideType().equals(ProvideTypeEnum.METHOD.getCode())) {
            try {
                Class<?>[] paramsTypes = null;
                Object[] argValues = null;

                if (StrUtil.isNotBlank(rule.getFormalParam()) && StrUtil.isNotBlank(rule.getActualParam())) {
                    // 获取形参数组
                    String[] formalArray = rule.getFormalParam().split(";");
                    // 获取实参数组
                    String[] actualArray = rule.getActualParam().split(";");

                    if (formalArray.length != actualArray.length)
                        throw new RuntimeException("形参数量与实参数量不符合");

                    // 转换形参为Class数组
                    paramsTypes = new Class<?>[formalArray.length];
                    for (int i = 0; i < formalArray.length; i++) {
                        paramsTypes[i] = Class.forName(formalArray[i].trim());
                    }

                    // 转换实参为Object数组
                    argValues = new Object[actualArray.length];
                    for (int i = 0; i < actualArray.length; i++) {
                        argValues[i] = JSONObject.parseObject(actualArray[i], paramsTypes[i]);
                    }
                }

                Class<?> clazz = Class.forName(rule.getClassName());
                Object result;

                Method targetMethod = clazz.getDeclaredMethod(rule.getMethodName(), paramsTypes);
                if (Modifier.isStatic(targetMethod.getModifiers())) {
                    // 设置静态方法可访问
                    targetMethod.setAccessible(true);
                    // 执行静态方法
                    result = targetMethod.invoke(null, argValues);
                } else {
                    try {
                        // 尝试从容器中获取实例
                        Object instance = context.getBean(Class.forName(rule.getClassName()));
                        Class<?> beanClazz = instance.getClass();
                        Method beanClazzMethod = beanClazz.getDeclaredMethod(rule.getMethodName(), paramsTypes);

                        // 执行方法
                        result = beanClazzMethod.invoke(instance, argValues);

                    } catch (NoSuchBeanDefinitionException e) {
                        // 创建类实例
                        Object obj = clazz.newInstance();
                        // 执行方法
                        result = targetMethod.invoke(obj, argValues);
                    }
                }

                dto.setResult(result);
                ruleList.add(dto);

            } catch (NoSuchMethodException e) {
                throw new RuntimeException("配置了不存在的方法");
            } catch (ClassNotFoundException e) {
                throw new RuntimeException("配置了不存在的类");
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("其他错误:" + e.getMessage());
            }

        } else
            throw new RuntimeException("错误的提供类型");
    }
    DataScopeInfo dataScopeInfo = new DataScopeInfo();
    dataScopeInfo.setRuleList(ruleList);
    return dataScopeInfo;
}

4 例子1 查看订单金额大于100且小于500的订单

规则配置

  1. 新增一个标记,可以理解成一个接口标识

342d7f6fe0e7ddaa4bed4dc11fa3e566.jpeg

图片
  1. 这个接口下所有的规则

e4630703c47d1e728c3bb36bbf2dc791.jpeg

图片
  1. 查看订单金额大于100且小于500的订单的需求的具体配置,这个配置的目的是通过反射执行com.gitee.whzzone.admin.business.service.impl.OrderServiceImpl这个类下的limitAmountBetween(BigDecimal, BigDecimal)的方法,也就是执行limitAmountBetween(100, 500),返回符合条件的orderIds,然后会在执行sql前去拼接 select ... from order where ... and id in ({这里是返回的orderIds}),从而实现这个权限控制

b3a69c51051ad5cca83d0f6ca7ada52c.jpeg

图片
  1. 给角色的这个订单列表接口配置查看订单金额大于100且小于500的订单这个规则,那么这个角色只能查看范围内的订单数据了。

b9cae12f57ea63a9022b963c45a41f28.jpeg

图片

代码

controller
@Api(tags = "订单相关")
@RestController
@RequestMapping("order")
public class OrderController extends EntityController<Order, OrderService, OrderDto, OrderQuery> {
    // 通用的增删改查不用写,父类已实现
}
service
public interface OrderService extends EntityService<Order, OrderDto, OrderQuery> {
    // 通用的增删改查不用写,父类已实现
    
    /**
     * 查询订单范围内的 orderIds
     * @param begin 订单金额开始
     * @param end 订单金额结束
     * @return
     */
    List<Long> limitAmountBetween(BigDecimal begin, BigDecimal end);
}
impl
@Service
public class OrderServiceImpl extends EntityServiceImpl<OrderMapper, Order, OrderDto, OrderQuery> implements OrderService {
    
    @DataScope("order-list") // 使用在方法上,交给AOP默认处理,标记这个方法为订单列表查询
    @Override // 重写父类列表查询
    public List<OrderDto> list(OrderQuery query) {
        LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(StrUtil.isNotBlank(query.getReceiverName()), Order::getReceiverName, query.getReceiverName());
        queryWrapper.eq(StrUtil.isNotBlank(query.getReceiverPhone()), Order::getReceiverPhone, query.getReceiverPhone());
        queryWrapper.eq(StrUtil.isNotBlank(query.getReceiverAddress()), Order::getReceiverAddress, query.getReceiverAddress());
        queryWrapper.eq(query.getOrderStatus() != null, Order::getOrderStatus, query.getOrderStatus());
        return afterQueryHandler(list(queryWrapper));
    }

    // 具体实现
    @Override
    public List<Long> limitAmountBetween(BigDecimal begin, BigDecimal end) {
        LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.between(Order::getOrderAmount, begin, end);
        List<Order> list = list(queryWrapper);
        if (CollectionUtil.isEmpty(list))
            return new ArrayList<>();

        return list.stream().map(BaseEntity::getId).collect(Collectors.toList());
    }
}

这样就实现了查看订单金额大于100且小于500的订单的需求,其实这个需求用不着这么麻烦,被我复杂化了(演示一下),其实用例子2的方式来实现。配两条规则:分别是order_amount > 100order_amount < 500的规则,然后选择AND连接就可以了。

5 例子2 查看收货人地址模糊查询钦南区的订单

规则配置

  1. 新增一个规则,提供类型,单表查询可以不设置表别名,看图吧

f4a4b3efa19b9634407cf22a5976cafd.jpeg

图片
  1. 配置角色在订单列表查询接口使用的规则

36a90ab00772bd6d58f7ab414d5379f6.jpeg

图片

代码

例子1的基础上不用做任何改动,因为这个需求无需编写代码

这样就实现了这个简单的需求,这样处理后,就可以在sql执行前拼接对应的查询条件,从而实现数据权限

到这里以上前面说的两个例子就可以搞定了,这查看全部数据自定义数据权限本部门数据权限本部门及以下数据仅本人数据权限五种权限在无形中实现了,针对你的用户id字段、部门id字段配几条对应的规则就可以。

6 当然,一键代码生成,一句代码都不用写即可,实现单表的增删改查

EntityController

public abstract class EntityController<T extends BaseEntity<T>, S extends EntityService<T, D, Q>, D extends EntityDto, Q extends EntityQuery> {
    @Autowired
    private S service;

    @RequestLogger
    @ApiOperation("获取")
    @GetMapping("/get/{id}")
    public Result<D> get(@PathVariable Long id){
        T t = service.getById(id);
        return Result.ok("操作成功", service.afterQueryHandler(t));
    }

    @RequestLogger
    @ApiOperation("删除")
    @GetMapping("/delete/{id}")
    public Result<Boolean> delete(@PathVariable Long id){
        return Result.ok("操作成功", service.removeById(id));
    }

    @RequestLogger
    @ApiOperation("保存")
    @PostMapping("save")
    public Result<T> save(@Validated(CreateGroup.class) @RequestBody D d){
        return Result.ok("操作成功", service.save(d));
    }

    @RequestLogger
    @ApiOperation("更新")
    @PostMapping("update")
    public Result<Boolean> update(@Validated(UpdateGroup.class) @RequestBody D d){
        return Result.ok("操作成功", service.updateById(d));
    }

    @RequestLogger
    @ApiOperation("分页")
    @PostMapping("page")
    public Result<PageData<D>> page(@RequestBody Q q){
        return Result.ok("操作成功", service.page(q));
    }

    @RequestLogger
    @ApiOperation("列表")
    @PostMapping("list")
    public Result<List<D>> list(@RequestBody Q q){
        return Result.ok("操作成功", service.list(q));
    }

}

EntityService

public interface EntityService<T extends BaseEntity<T>, D extends EntityDto, Q extends EntityQuery> extends IService<T> {

    T save(D d);

    boolean updateById(D d);

    @Override
    T getById(Serializable id);

    @Override
    boolean removeById(T entity);

    @Override
    boolean removeById(Serializable id);

    T afterSaveHandler(T t);

    T afterUpdateHandler(T t);

    D afterQueryHandler(T t);

    List<D> afterQueryHandler(List<T> list);

    void afterDeleteHandler(T t);

    default Class<T> getTClass() {
        return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), EntityService.class, 0);
    }

    default Class<D> getDClass() {
        return (Class<D>) ReflectionKit.getSuperClassGenericType(this.getClass(), EntityService.class, 1);
    }

    default Class<Q> getQClass() {
        return (Class<Q>) ReflectionKit.getSuperClassGenericType(this.getClass(), EntityService.class, 2);
    }

    boolean isExist(Long id);

    D beforeSaveOrUpdateHandler(D d);

    D beforeSaveHandler(D d);

    D beforeUpdateHandler(D d);

    PageData<D> page(Q q);

    QueryWrapper<T> queryWrapperHandler(Q q);

    List<D> list(Q q);

}

EntityServiceImpl

public abstract class EntityServiceImpl<M extends BaseMapper<T>, T extends BaseEntity<T>, D extends EntityDto, Q extends EntityQuery> extends ServiceImpl<M, T> implements EntityService<T, D, Q> {

    @Override
    @Transactional(rollbackFor = Exception.class)
    public T save(D d) {
        try {
            d = beforeSaveOrUpdateHandler(d);
            d = beforeSaveHandler(d);

            Class<T> dClass = getTClass();
            T t = dClass.getDeclaredConstructor().newInstance();

            BeanUtil.copyProperties(d, t);
            boolean save = save(t);
            if (!save) {
                throw new RuntimeException("操作失败");
            }
            afterSaveHandler(t);
            return t;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateById(D d) {
        try {
            d = beforeSaveOrUpdateHandler(d);
            d = beforeUpdateHandler(d);

            Class<D> dClass = getDClass();
            Class<? super D> superclass = dClass.getSuperclass();
            Field fieldId = superclass.getDeclaredField("id");
            fieldId.setAccessible(true);
            long id = (long) fieldId.get(d);
            T t = getById(id);
            if (t == null) {
                throw new RuntimeException(StrUtil.format("【{}】不存在", id));
            }

            BeanUtil.copyProperties(d, t);
            boolean b = super.updateById(t);
            if (b) {
                afterUpdateHandler(t);
            }
            return b;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    public T getById(Serializable id) {
        if (id == null)
            return null;
        return super.getById(id);
    }

    @Override
    public boolean removeById(T entity) {
        return removeById(entity.getId());
    }

    @Override
    public boolean removeById(Serializable id) {
        if (id == null) {
            throw new RuntimeException("id不能为空");
        }

        T t = getById(id);

        boolean b = SqlHelper.retBool(getBaseMapper().deleteById(id));

        if (b) {
            afterDeleteHandler(t);
        }
        return b;
    }

    @Override
    public T afterSaveHandler(T t) {
        return t;
    }

    @Override
    public T afterUpdateHandler(T t) {
        return t;
    }

    @Override
    public D afterQueryHandler(T t) {
        Class<D> dClass = getDClass();
        return BeanUtil.copyProperties(t, dClass);
    }

    @Override
    public List<D> afterQueryHandler(List<T> list) {
        List<D> dList = new ArrayList<>();

        if (CollectionUtil.isEmpty(list)) {
            return dList;
        }

        for (T t : list) {
            D d = afterQueryHandler(t);
            dList.add(d);
        }
        return dList;
    }

    @Override
    public void afterDeleteHandler(T t) {

    }

    @Override
    public boolean isExist(Long id) {
        if (id == null)
            throw new RuntimeException("id 为空");

        long count = count(new QueryWrapper<T>().eq("id", id));
        return count > 0;
    }

    @Override
    public D beforeSaveOrUpdateHandler(D d) {
        return d;
    }

    @Override
    public D beforeSaveHandler(D d) {
        return d;
    }

    @Override
    public D beforeUpdateHandler(D d) {
        return d;
    }

    @Override
    public PageData<D> page(Q q) {
        try {
            QueryWrapper<T> queryWrapper = queryWrapperHandler(q);

            IPage<T> page = new Page<>(q.getCurPage(), q.getPageSize());

            page(page, queryWrapper);

            List<D> dList = afterQueryHandler(page.getRecords());

            return new PageData<>(dList, page.getTotal(), page.getPages());

        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    public QueryWrapper<T> queryWrapperHandler(Q q) {
        try {
            Class<? extends EntityQuery> qClass = q.getClass();

            Field[] fields = qClass.getDeclaredFields();

            QueryWrapper<T> queryWrapper = new QueryWrapper<>();

            Map<String, Field[]> betweenFieldMap = new HashMap<>();

            // 处理@SelectColumn
            SelectColumn selectColumn = qClass.getAnnotation(SelectColumn.class);
            if (selectColumn != null && selectColumn.value() != null && selectColumn.value().length > 0) {
                String[] strings = selectColumn.value();
                for (int i = 0; i < strings.length; i++) {
                    strings[i] = StrUtil.toUnderlineCase(strings[i]);
                }
                queryWrapper.select(strings);
            }

            String sortColumn = "";
            String sortOrder = "";

            for (Field field : fields) {
                // if (isBusinessField(field.getName())) {
                field.setAccessible(true);
                Object value = field.get(q);

                // 判断该属性是否存在值
                if (Objects.isNull(value) || String.valueOf(value).equals("null") || value.equals("")) {
                    continue;
                }

                // FIXME 存在bug,应该在判空前执行
                // 是否存在注解@QuerySort
                QuerySort querySort = field.getDeclaredAnnotation(QuerySort.class);
                if (querySort != null) {
                    String paramValue = (String) field.get(q);
                    sortColumn = paramValue.isEmpty() ? querySort.value() : paramValue;
                }

                // 是否存在注解@QueryOrder
                QueryOrder queryOrder = field.getDeclaredAnnotation(QueryOrder.class);
                if (queryOrder != null) {
                    String paramValue = (String) field.get(q);
                    sortOrder = paramValue.isEmpty() ? queryOrder.value() : paramValue;
                }

                // 是否存在注解@Query
                Query query = field.getDeclaredAnnotation(Query.class);
                if (query == null) {
                    continue;
                }

                String columnName = StrUtil.isBlank(query.column()) ? StrUtil.toUnderlineCase(field.getName()) : query.column();
                // TODO 待优化这坨屎山
                if (query.expression().equals(ExpressionEnum.EQ)) {
                    queryWrapper.eq(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.NE)) {
                    queryWrapper.ne(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.LIKE)) {
                    queryWrapper.like(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.GT)) {
                    queryWrapper.gt(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.GE)) {
                    queryWrapper.ge(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.LT)) {
                    queryWrapper.lt(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.LE)) {
                    queryWrapper.le(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.IN)) {
                    queryWrapper.in(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.NOT_IN)) {
                    queryWrapper.notIn(columnName, value);
                } else if (query.expression().equals(ExpressionEnum.IS_NULL)) {
                    queryWrapper.isNull(columnName);
                } else if (query.expression().equals(ExpressionEnum.NOT_NULL)) {
                    queryWrapper.isNotNull(columnName);
                } else if (query.expression().equals(ExpressionEnum.BETWEEN)) {
                    if (betweenFieldMap.containsKey(columnName)) {
                        Field[] f = betweenFieldMap.get(columnName);
                        Field[] tempList = new Field[2];
                        tempList[0] = f[0];
                        tempList[1] = field;
                        betweenFieldMap.put(columnName, tempList);
                    } else {
                        betweenFieldMap.put(columnName, new Field[]{field});
                    }
                }

            }
            // }

            Set<String> keySet = betweenFieldMap.keySet();
            for (String key : keySet) {
                // 已在编译时做了相关校验,在此无须做重复且耗时的校验
                Field[] itemFieldList = betweenFieldMap.get(key);
                if (itemFieldList.length != 2){
                    throw new IllegalArgumentException("查询参数数量对应异常");
                }

                Field field1 = itemFieldList[0];
                Field field2 = itemFieldList[1];

                Query query1 = field1.getDeclaredAnnotation(Query.class);

                if (field1.get(q) instanceof Date) {
                    if (query1.left()) {
                        queryWrapper.apply("date_format(" + key + ",'%y%m%d') >= date_format({0},'%y%m%d')", field1.get(q));
                        queryWrapper.apply("date_format(" + key + ",'%y%m%d') <= date_format({0},'%y%m%d')", field2.get(q));
                    } else {
                        queryWrapper.apply("date_format(" + key + ",'%y%m%d') <= date_format({0},'%y%m%d')", field1.get(q));
                        queryWrapper.apply("date_format(" + key + ",'%y%m%d') >= date_format({0},'%y%m%d')", field2.get(q));
                    }
                } else {
                    if (query1.left()) {
                        queryWrapper.between(key, field1.get(q), field2.get(q));
                    } else {
                        queryWrapper.between(key, field2.get(q), field1.get(q));
                    }
                }
            }

            if (sortOrder.equalsIgnoreCase("desc")) {
                queryWrapper.orderByDesc(StrUtil.isNotBlank(sortColumn), StrUtil.toUnderlineCase(sortColumn));
            } else {
                queryWrapper.orderByAsc(StrUtil.isNotBlank(sortColumn), StrUtil.toUnderlineCase(sortColumn));
            }

            return queryWrapper;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    public List<D> list(Q q) {
        try {
            QueryWrapper<T> queryWrapper = queryWrapperHandler(q);
            return afterQueryHandler(list(queryWrapper));
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }
}

这个怎么说呢,还是方便的,生成代码后就启动了,增删改查的接口就有了,如果自定义业务,重写controller、service实现就可以。

自定义注解

也设计了一些自定义注解来进行辅助查询

// 查询特定字段
@SelectColumn({"id", "create_time", "create_by", "update_time", "update_by", "deleted", "dict_name", "dict_code", "dict_type", "sort", "remark"})
@ApiModel(value = "DictQuery对象", description = "系统字典")
public class DictQuery extends EntityQuery {

    @Query(expression = ExpressionEnum.LIKE) // dictName字段模糊查询
    @ApiModelProperty("字典名称")
    private String dictName;

    @Query // dictCode字段精确查询
    @ApiModelProperty("字典编码(唯一)")
    private String dictCode;

    @Query
    @ApiModelProperty("字典类型,0-列表,1-树")
    private Integer dictType;

    @ApiModelProperty("排序")
    private Integer sort;

    @ApiModelProperty("备注")
    private String remark;

    // create_time 字段范围查询
    @Query(column = "create_time", expression = ExpressionEnum.BETWEEN, left = true)
    @ApiModelProperty("开始日期")
    private Date beginDate;
    
    // create_time 字段范围查询
    @Query(column = "create_time", expression = ExpressionEnum.BETWEEN, left = false)
    @ApiModelProperty("结束日期")
    private Date endDate;

    @QuerySort("sort") // 如果sortColumn为null,根据sort字段排序
    @ApiModelProperty("排序字段")
    private String sortColumn;

    @QueryOrder // 如果sortOrder为null,默认asc
    @ApiModelProperty("排序方式-asc/desc")
    private String sortOrder;

}

还有很多可以完善的地方,忽略我的垃圾技术,本文主要是描述一下思路。有兴趣的可以移步到仓库看看,一个人维护好难啊,但是也不想放弃,写都写到这是吧,总感觉一个权限管理系统就要成了哈哈哈哈哈 有没有感兴趣的来一起维护维护,非常欢迎~

[7 项目地址 wonder-server: 一个有意思的权限管理系统2]

参考资料

[1]数据权限就该这么实现(设计篇): https://juejin.cn/post/7127810938161332232

[2]wonder-server: 一个有意思的权限管理系统: https://gitee.com/whzzone/wonder-server

👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡 /  赠书福利

全栈前后端分离博客项目 1.0 版本完结啦,2.0 正在更新中..., 演示链接http://116.62.199.48/ ,全程手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,直到项目上线。目前已更新了189小节,累计33w+字,讲解图:1308张,还在持续爆肝中.. 后续还会上新更多项目,目标是将Java领域典型的项目都整一波,如秒杀系统, 在线商城, IM即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,已有950+小伙伴加入(早鸟价超低)

90a6746e794d43901c3e2f99d2f1f745.gif

74e2e448d810ea240236e2e4984c3826.jpeg

 
 

c0ad7925ae4744919e7d034ed2414997.gif

 
 
 
 
1. 我的私密学习小圈子~
2. Prometheus+Grafana+NodeExporter 太强了!
3. 面试官:Feign 第一次调用为什么会很慢?大部分人都答不上来!
4. 从B 树、B+ 树、B* 树谈到R 树
 
 
最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。
PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。
点“在看”支持小哈呀,谢谢啦
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,记录Mybatis执行的增删改操作的前后数据变化,可以使用AOP拦截Mybatis的Mapper接口的对应方法,并在拦截器中获取方法的参数和返回值,从而实现数据变化的记录。 以下是一个示例代码,演示如何使用AOP拦截Mybatis的Mapper接口的insert、update和delete方法,并记录前后数据变化。 1. 定义一个AOP切面类,实现增删改方法的拦截和数据变化的记录: ```java @Aspect @Component public class MybatisDataChangeAspect { // 定义切入点,拦截Mybatis的Mapper接口的insert、update和delete方法 @Pointcut("execution(* com.example.mapper.*Mapper.insert*(..)) " + "|| execution(* com.example.mapper.*Mapper.update*(..)) " + "|| execution(* com.example.mapper.*Mapper.delete*(..)) ") public void pointcut() {} // 定义环绕通知 @Around("pointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 获取方法参数 Object[] args = joinPoint.getArgs(); // 获取方法返回值 Object result = joinPoint.proceed(); // 记录数据变化 logDataChange(joinPoint, args, result); return result; } // 记录数据变化 private void logDataChange(ProceedingJoinPoint joinPoint, Object[] args, Object result) { // 获取Mapper接口名和方法名 String mapperName = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); // 获取方法参数名 String[] paramNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames(); // 获取方法参数值 Map<String, Object> paramMap = new HashMap<>(); for (int i = 0; i < args.length; i++) { paramMap.put(paramNames[i], args[i]); } // 获取返回值 String returnValue = ""; if (result != null) { returnValue = result.toString(); } // 记录数据变化 System.out.println(String.format("Mapper [%s] 的方法 [%s] 执行了数据变化," + "参数为 %s,返回值为 %s", mapperName, methodName, paramMap, returnValue)); } } ``` 2. 在Spring Boot的配置文件中开启AOP: ```yaml spring: aop: auto: true ``` 3. 在Mapper接口中定义对应的增删改方法: ```java @Mapper public interface UserMapper { int insertUser(User user); int updateUser(User user); int deleteUser(int id); } ``` 4. 在Controller中调用Mapper接口的方法: ```java @RestController public class UserController { @Autowired private UserMapper userMapper; @PostMapping("/users") public String addUser(@RequestBody User user) { userMapper.insertUser(user); return "success"; } @PutMapping("/users") public String updateUser(@RequestBody User user) { userMapper.updateUser(user); return "success"; } @DeleteMapping("/users/{id}") public String deleteUser(@PathVariable int id) { userMapper.deleteUser(id); return "success"; } } ``` 这样,当Controller中调用Mapper接口的增删改方法时,AOP切面会拦截这些方法,并记录前后数据变化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值