概念
aop(切面编程)
-
aop关键的四个概念就是:切面、连接点、通知、切入点
- Aspect:切面,就是切入系统的一个切面
- join point:连接点,方法的开始
- Advice:通知,切面在某个连接点执行的操作
- before : 前置
- After returning : 正常返回后
- After throwing : 异常返回后
- After : 后置无论正常异常都执行
- Around : 环绕
- Pointcut:切点,符合切点表达式的连接点,也就是真正被切入的地方
注解
可以理解为java的标记,可以对任何类方法变量等进行注解标记,然后通过标记获取标注的内容
应用示例
拦截sql实现数据权限控制
在业务的数据库查询中增加用户或者组织等权限范围,实现不改动业务代码的情况下实现区分用户的数据权限控制
- 创建自定义注解,标注字段(用户权限字段、用户查询条件适用、部门权限字段、部门查询条件适用)
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;
}
- 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();
}
}
- 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 ");
}
}
- 在service方法中加入注解@DataPermission(userField = "user_id") ,即可在方法中的数据库语句中进行添加数据权限
实现列表重排序
自定义排序规则列表中,使用的排序字段随着列表数据的增删改查实现自动重排序,以岗位列表举例
- 创建自定义注解,标注操作类型(删除操作需要将序号之后的内容提前,新增和修改需要将排序后的内容整体向后加一)
/**
* 自定义注解:岗位重排序
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface PostSort {
//操作类型:delete saveOrUpdate
String operation() default "";
}
- 给需要监听的位置(controller的方法)加上自定义注解
/**
* 修改 岗位表
*/
@PostMapping("/update")
@PostSortAnnotation(operation = "saveOrUpdate")
public R update(@Valid @RequestBody Post post) {
CacheUtil.clear(SYS_CACHE);
return R.status(postService.updateById(post));
}
- 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);
//发消息改变用户顺序
}
}
- 业务相关代码,可忽略
@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>