自定义注解+切面编程使用

112 阅读6分钟

概念

aop(切面编程)

  • aop关键的四个概念就是:切面、连接点、通知、切入点

    • Aspect:切面,就是切入系统的一个切面
    • join point:连接点,方法的开始
    • Advice:通知,切面在某个连接点执行的操作
      • before : 前置
      • After returning : 正常返回后
      • After throwing : 异常返回后
      • After : 后置无论正常异常都执行
      • Around : 环绕
    • Pointcut:切点,符合切点表达式的连接点,也就是真正被切入的地方

注解

可以理解为java的标记,可以对任何类方法变量等进行注解标记,然后通过标记获取标注的内容

应用示例

拦截sql实现数据权限控制

在业务的数据库查询中增加用户或者组织等权限范围,实现不改动业务代码的情况下实现区分用户的数据权限控制

  1. 创建自定义注解,标注字段(用户权限字段、用户查询条件适用、部门权限字段、部门查询条件适用)

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {

    // 用户字段,例如 "creator_id"
    String userField() default "";

    // 用户条件字段,例如 true "in" / false "find_in_set"
    boolean userConditionField() default true;

    // 部门字段,例如 "dept_id"
    String deptField() default "";

    // 用户条件字段,例如 true "in" / false "find_in_set"
    boolean deptConditionField() default true;
}
  1. Aspect 实现数据权限范围的获取和mapper拦截器的注册

import com.alibaba.cloud.commons.lang.StringUtils;
import com.starlinkdt.hrm.aop.Interceptor.DataPermissionContext;
import com.starlinkdt.hrm.aop.annotation.DataPermission;
import com.starlinkdt.hrm.config.UserContext;
import com.starlinkdt.hrm.model.constanst.YesNoEnum;
import com.starlinkdt.hrm.service.impl.DataPermissionService;
import jakarta.annotation.Resource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.util.Set;
import java.util.stream.Collectors;

@Aspect
@Component
public class DataPermissionAspect {

    @Resource
    private DataPermissionService dataPermissionService;

    @Around("@annotation(dataPermission)")
    public Object around(ProceedingJoinPoint joinPoint, DataPermission dataPermission) throws Throwable {
        // 用户和部门权限条件
        String userField = dataPermission.userField();
        String deptField = dataPermission.deptField();
        boolean userConditionField = dataPermission.userConditionField();
        boolean deptConditionField = dataPermission.deptConditionField();
        //  如果没有权限条件,则直接返回
        if ("".equals(userField) && "".equals(deptField)) {
            return joinPoint.proceed();
        }
        // 获取当前用户
        Long currentUserId = getCurrentUserId();
        // 拼接的sql语句
        StringBuilder condition = new StringBuilder();
        // 用户权限条件
        if (StringUtils.isNotEmpty(userField)) {
            // 用户权限范围
            Set<String> userScope = dataPermissionService.getUserPermissionScope(currentUserId);
            String userCond = buildPermissionCondition(userScope, userField, userConditionField);
            if (!YesNoEnum.NO.getCode().equals(userCond)) {
                condition.append(userCond);
            }
        }
        // 部门权限条件
        if (StringUtils.isNotEmpty(deptField)) {
            // 部门权限范围
            Set<String> deptScope = dataPermissionService.getDeptPermissionScope(currentUserId);
            String deptCond = buildPermissionCondition(deptScope, deptField, deptConditionField);
            if (!YesNoEnum.NO.getCode().equals(deptCond)) {
                condition.append(deptCond);
            }
        }
        // mapper拦截器
        try {
            DataPermissionContext.setPermissionCondition(condition.toString());
            return joinPoint.proceed();
        } finally {
            DataPermissionContext.clear();
        }
    }

    // 获取当前用户id
    private Long getCurrentUserId() {
        return UserContext.getUserId();
    }

    /**
     * 构建权限条件(支持用户/部门)
     */
    private String buildPermissionCondition(Set<String> scope, String field, boolean conditionField) {
        if (scope == null || scope.isEmpty()) {
            return YesNoEnum.NO.getCode();
        }
        StringBuilder sb = new StringBuilder();
        if (!conditionField) {
            // 使用 FIND_IN_SET 模式
            for (String id : scope) {
                sb.append(String.format("FIND_IN_SET('%s', %s) OR ", id, field));
            }
            if (!sb.isEmpty()) {
                sb.setLength(sb.length() - 4); // 去掉最后的 OR
            }
        } else {
            // 使用 IN 模式
            String inClause = scope.stream()
                    .map(id -> "'" + id + "'")
                    .collect(Collectors.joining(", "));
            sb.append(field).append(" IN (").append(inClause).append(")");
        }
        return sb.toString();
    }

}
  1. mapper上下文和拦截器

import org.springframework.context.annotation.Configuration;

@Configuration
public class DataPermissionContext {
    private static final ThreadLocal<String> context = new ThreadLocal<>();

    public static void setPermissionCondition(String condition) {
        context.set(condition);
    }

    public static String getPermissionCondition() {
        return context.get();
    }

    public static void clear() {
        context.remove();
    }
}
import com.starlinkdt.hrm.utils.ReflectUtil;
import lombok.Getter;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;

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

@Getter
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class DataPermissionInterceptor implements Interceptor {

    private final DataPermissionContext dataPermissionContext;

    public DataPermissionInterceptor(DataPermissionContext dataPermissionContext) {
        this.dataPermissionContext = dataPermissionContext;
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();

        // 获取当前拼接的权限条件
        String permissionCondition = DataPermissionContext.getPermissionCondition();
        if (permissionCondition != null && !permissionCondition.isEmpty()) {
            String originalSql = boundSql.getSql();
            String newSql = injectPermissionCondition(originalSql, permissionCondition);
            // 使用反射替换 SQL
            ReflectUtil.setFieldValue(boundSql, "sql", newSql);
        }

        return invocation.proceed();
    }

    /**
     * 将权限条件注入到原始 SQL 中
     */
    private String injectPermissionCondition(String originalSql, String condition) {
        originalSql = originalSql.trim();
        if (condition == null || condition.isEmpty()) {
            return originalSql;
        }

        // 只对 SELECT 查询添加权限条件
        if (!isSelectStatement(originalSql)) {
            return originalSql;
        }

        String lowerSql = originalSql.toLowerCase();

        // 判断是否已有 WHERE/HAVING
        boolean hasWhere = containsKeywordAtBoundary(lowerSql, "where", "having");

        // 查找第一个出现的关键字:WHERE / HAVING / ORDER BY / LIMIT
        int whereIndex = indexOfKeywordAtBoundary(lowerSql, "where", "having");

        // 构建最终要插入的位置
        int insertPos;

        if (hasWhere) {
            // 如果已经有 WHERE/HAVING,查找下一个关键字(ORDER BY / LIMIT)的位置
            int nextClauseIndex = findNextClauseIndexAtBoundary(lowerSql, whereIndex);

            if (nextClauseIndex != -1) {
                // 插入到 WHERE 和下一个关键字之间
                return new StringBuilder(originalSql)
                        .insert(nextClauseIndex, " AND (" + condition + ")")
                        .toString();
            } else {
                // 没有后续关键字,直接追加到 WHERE 后面
                return originalSql + " AND (" + condition + ")";
            }
        } else {
            // 没有 WHERE,查找第一个出现的关键字:ORDER BY / LIMIT
            int firstClauseIndex = findFirstKeywordAtBoundary(lowerSql, "order by", "limit");

            if (firstClauseIndex == -1) {
                // 没有任何关键字,直接加上 WHERE 条件
                return originalSql + " WHERE (" + condition + ")";
            } else {
                // 在第一个关键字前插入 WHERE 条件
                return new StringBuilder(originalSql)
                        .insert(firstClauseIndex, " WHERE (" + condition + ") ")
                        .toString();
            }
        }
    }


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

    @Override
    public void setProperties(Properties properties) {
    }

    // 工具方法:判断是否包含任意一个关键字
    private boolean containsKeywordAtBoundary(String sql, String... keywords) {
        for (String keyword : keywords) {
            int index = sql.indexOf(keyword);
            while (index >= 0) {
                if (isWordBoundary(sql, index, keyword.length())) {
                    return true;
                }
                index = sql.indexOf(keyword, index + keyword.length());
            }
        }
        return false;
    }

    // 工具方法:查找第一个出现的关键字索引
    private int indexOfKeywordAtBoundary(String sql, String... keywords) {
        int minIndex = -1;
        for (String keyword : keywords) {
            int index = sql.indexOf(keyword);
            while (index >= 0) {
                if (isWordBoundary(sql, index, keyword.length())) {
                    if (minIndex == -1 || index < minIndex) {
                        minIndex = index;
                    }
                    break;
                }
                index = sql.indexOf(keyword, index + keyword.length());
            }
        }
        return minIndex;
    }

    // 方法:查找从某个位置开始的第一个关键字索引
    private int findNextClauseIndexAtBoundary(String sql, int startPos) {
        int minIndex = -1;
        String subSql = sql.substring(startPos);
        int offset = startPos;

        int obIndex = indexOfKeywordAtBoundary(subSql, "order by");
        if (obIndex >= 0 && (minIndex == -1 || obIndex + offset < minIndex)) {
            minIndex = obIndex + offset;
        }

        int hgIndex = indexOfKeywordAtBoundary(subSql, "having");
        if (hgIndex >= 0 && (minIndex == -1 || hgIndex + offset < minIndex)) {
            minIndex = hgIndex + offset;
        }

        int limIndex = indexOfKeywordAtBoundary(subSql, "limit");
        if (limIndex >= 0 && (minIndex == -1 || limIndex + offset < minIndex)) {
            minIndex = limIndex + offset;
        }

        return minIndex;
    }


    // 查找第一个出现的关键字索引(用于无 WHERE 的情况)
    private int findFirstKeywordAtBoundary(String sql, String... keywords) {
        int minIndex = -1;
        for (String keyword : keywords) {
            int index = sql.indexOf(keyword);
            while (index >= 0) {
                if (isWordBoundary(sql, index, keyword.length())) {
                    if (minIndex == -1 || index < minIndex) {
                        minIndex = index;
                    }
                    break;
                }
                index = sql.indexOf(keyword, index + keyword.length());
            }
        }
        return minIndex;
    }

    // 辅助方法:判断是否是单词边界
    private boolean isWordBoundary(String sql, int pos, int keywordLength) {
        if (pos > 0 && Character.isLetterOrDigit(sql.charAt(pos - 1))) {
            return false;
        }
        int endPos = pos + keywordLength;
        if (endPos < sql.length() && Character.isLetterOrDigit(sql.charAt(endPos))) {
            return false;
        }
        return true;
    }

    // 获取某个关键字的长度(忽略大小写)
    private int getKeywordLengthAfter(String sql, int pos, String keyword) {
        if (sql.regionMatches(true, pos, keyword, 0, keyword.length())) {
            return keyword.length();
        }
        return 0;
    }


    // 判断是否是 SELECT 查询
    private boolean isSelectStatement(String sql) {
        String trimmed = sql.trim().toLowerCase();
        return trimmed.startsWith("select ");
    }
}
  1. 在service方法中加入注解@DataPermission(userField = "user_id") ,即可在方法中的数据库语句中进行添加数据权限

实现列表重排序

自定义排序规则列表中,使用的排序字段随着列表数据的增删改查实现自动重排序,以岗位列表举例

  1. 创建自定义注解,标注操作类型(删除操作需要将序号之后的内容提前,新增和修改需要将排序后的内容整体向后加一)
/**
 * 自定义注解:岗位重排序
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface PostSort {

   //操作类型:delete saveOrUpdate
   String operation() default "";

}
  1. 给需要监听的位置(controller的方法)加上自定义注解
/**
 * 修改 岗位表
 */
@PostMapping("/update")
@PostSortAnnotation(operation = "saveOrUpdate")
public R update(@Valid @RequestBody Post post) {
   CacheUtil.clear(SYS_CACHE);
   return R.status(postService.updateById(post));
}
  1. Aspect实现业务逻辑(挂切入点、获取参数、业务实现)
package org.springblade.system.aop;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springblade.system.annotation.PostSortAnnotation;
import org.springblade.system.entity.Post;
import org.springblade.system.service.impl.PostServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;

@Component
@Aspect
@Slf4j
public class PostAspect {

   @Autowired
   private PostServiceImpl postService;

   /**
    * 挂切入点
    */
   @Pointcut("@annotation(org.springblade.system.annotation.PostSortAnnotation)")
   public void postAspect() {
   }

   /**
    * 前置通知
    */
   @Before("postAspect()")
   public void doBefore(JoinPoint jp) {
      System.out.println("进入前置通知");
      //获取监听的方法
      MethodSignature signature = (MethodSignature) jp.getSignature();
      Method method = signature.getMethod();
      //方法注解getAnnotation 参数注解getParamAnnotation,一维是参数,二维是参数上的注解
      PostSortAnnotation annotation = method.getAnnotation(PostSortAnnotation.class);
      if (annotation != null) {
         //获取自定义注解的值
         String operation = annotation.operation();
         //获取方法的参数
         Object[] args = jp.getArgs();
         String requestParam = Arrays.toString(args);
         JSONObject jsonObject = (JSONObject) JSON.toJSON(args[0]);
         Map<String, Object> requestParamMap = JSON.toJavaObject(jsonObject, Map.class);
         System.out.println("请求参数:" + requestParamMap);
         //排序字段
         Integer sort = null;
         if (requestParamMap.containsKey("sort")) {
            Object sortObject = requestParamMap.get("sort");
            if (sortObject != null) {
               sort = Integer.valueOf(sortObject.toString());
            }
         }
         Long id = null;
         if (requestParamMap.containsKey("id")) {
            Object idObject = requestParamMap.get("id");
            if (idObject != null) {
               id = Long.valueOf(idObject.toString());
            }
         }
         if ("saveOrUpdate".equals(operation)) {
            //增加排序
            if (sort != null) {
               if (id != null) {
                  Post oldPost = postService.getById(id);
                  Boolean isUp = true;
                  if (sort < oldPost.getSort()) {
                     isUp = true;
                  } else {
                     isUp = false;
                  }
                  postService.sort(sort, isUp);
               }else {
                  postService.sort(sort, true);
               }
            }
         }
      }
   }

   /**
    * 后置通知
    */
   @After("postAspect()")
   public void doAfter(JoinPoint jp) {
      System.out.println("进入后置通知");
      //重排序
      postService.sort(0,true);
      //发消息改变用户顺序
   }

}
  1. 业务相关代码,可忽略
@Override
@Transactional
public Boolean sort(Integer sort, Boolean isUp) {
   //重排序
   if (sort == null) {
      sort = 0;
   }
   if (sort == 0) {
      List<Post> postList = this.list();
      List<Post> sortList = postList.stream().sorted(Comparator.comparing(Post::getSort)).collect(Collectors.toList());
      Integer num = 0;
      for (Post post : sortList) {
         num = num + 1;
         post.setSort(num);
      }
      //批量修改顺序
      updateBatchById(sortList);
   } else {
      if (isUp) {
         baseMapper.upSort(sort);
      } else {
         baseMapper.downSort(sort);
      }
   }
   redisTemplate.delete("post:sort:map");
   return true;
}

<update id="upSort" parameterType="java.lang.Integer">
    UPDATE blade_post SET sort = sort + 1 WHERE sort >= #{sort};
</update>

<update id="downSort" parameterType="java.lang.Integer">
    UPDATE blade_post SET sort = sort - 1 WHERE sort <![CDATA[ <= ]]> #{sort};
</update>