Java - 使用AOP+SpEL基于DB中的用户ID自动补全用户姓名
Java - 使用AOP+SpEL基于DB中的用户ID自动补全用户姓名
文章目录
- Java - 使用AOP+SpEL基于DB中的用户ID自动补全用户姓名
- 一、引言
- 二、环境
- 三、基本思路
- 四、实现过程
- 1. 确定切入点;
- 2. 基于自定义注解,注册切入点;
- 3. 在实体类上标记依赖关系;
- 4. 编写拦截器拦截,更新返回参数中的值;
- 五、完整代码
- 1. 自定义注解@UserAttrDependence
- 2. 自定义注解@UserAttrPlayback
- 3. 拦截器UserAttrPlaybackInterceptor
- 4. 在表实体类上标记需要回显的属性
- 总结
一、引言
在业务系统开发过程中,当向数据库写入一条记录时,通常会添加“创建人”和“创建时间”两个字段,以便于用户问题的定位和操作的回溯。
然而,在页面展示时,往往需要呈现友好的用户标识,如用户姓名或登录账号等。
尽管我们的数据库中仅存储了UserId,而表中一般不会重复存储用户名、姓名等字段,这便带来了一定的转换需求。
常规做法是在返回记录前,遍历整个List,通过UserId查询用户信息,然后将这些信息回写到数据传输对象(Dto)的相应字段中。
下面,我将介绍一种面向切面的实现策略,这种方法可以一劳永逸地简化流程,只需通过简单的配置,即可完成ID与Name的自动转换,从而提升开发效率和系统的整体性能。
二、环境
- Springboot 3.2.2
- jdk-17.0.9
- Maven 3.3.9
- windows 10
- ORM Mybatis + Mybatis-plus
三、基本思路
在前述内容中,我们提到了采用AOP(面向切面编程)结合SpEL(Spring表达式语言)的方法来实现功能。具体而言,AOP的作用在于拦截特定的方法,并对其返回值进行修改。而SpEL的运用则体现在实体类的目标属性上,通过SpEL表达式来描述ID属性到所需转换的依赖关系。
这两个关键技术的结合,为我们提供了实现自动转换的基础。接下来的步骤就是在拦截器的实现中,首先获取实体中的ID值,然后利用该ID值查询相应的用户信息,最后将查询到的用户名回写到实体中,完成属性的自动填充。
下面,我将详细分步骤阐述这一实现过程。
四、实现过程
1. 确定切入点;
切入点设计在Service层的实现类的方法上,因为这层数据比较单一,没有其复杂的Dto对象,基本上只有Entity或List两种类型,对于拦截处理逻辑的实现也比较友好。
@Override
@UserAttrPlayback
public UserInfo getUserInfoInfo(String userId) {
UserInfo info = UserInfoMapper.selectInfo(userId);
return info;
}
@Override
@UserAttrPlayback
public List<UserInfo> getUserInfoList() {
IList<SrDtLabel> List = new List<>();
List<UserInfo> userList = UserInfoMapper.getUserList();
return userList;
}
2. 基于自定义注解,注册切入点;
切入点的可以通过“表达式”来匹配,也可以通过指定类型注解去匹配,我选择了注解的方式,适合这个场景,通过注解可以按需指定需要补全的方法。
@AfterReturning(returning="rst"
, pointcut="@annotation(com.example.aspect.userinfo.UserAttrPlayback)")
public Object AfterExec(JoinPoint joinPoint,Object rst) throws IllegalAccessException {
if(rst==null)
return rst;
var isList = rst instanceof Collection;
if(isList){
List<Object> list = (List<Object>) rst;
list.forEach(x -> {
try {
writeBack(joinPoint,x);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
}else{
writeBack(joinPoint,rst);
}
return rst;
}
3. 在实体类上标记依赖关系;
实体类上待回显属性,依然通过自定义注解的形式标记,这样做有两个好处。一是确定了需要回显的属性,二是可以在属性上标记回显依赖的其它属性。
其中依赖关系使用SpEL表达式的形式描述,这样可以通过SeEL解析表达式,取到依赖的ID值,这样可以避免了写反射查询值的逻辑。
SpEL中标准的跟对象标识符是#this和#root,这里的#parm是基于评估对象(StandardEvaluationContext)自定义声明的参数,后边代码会有体现声明过程。
public class UserInfo{
private String userId;
@UserAttrDependence("#parm.userId") //自定义标记需要回显的属性和依赖的属性;
@TableField(exist = false) //标记该字段在库表中不存在
private String displayName;
}
4. 编写拦截器拦截,更新返回参数中的值;
拦截器目前只对Entity和List两种类型的返回值进行处理,基本能满足大部分场景需要了。
其中需要注意的是,基于反射向私有属性中写入值时,需要在写入前标记accessible属性为true可访问。
@AfterReturning(returning="rst"
, pointcut="@annotation(com.example.aspect.userinfo.UserAttrPlayback)")
public Object AfterExec(JoinPoint joinPoint,Object rst) throws IllegalAccessException {
if(rst==null)
return rst;
var isList = rst instanceof Collection;
if(isList){
List<Object> list = (List<Object>) rst;
list.forEach(x -> {
try {
writeBack(joinPoint,x);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
}else{
writeBack(joinPoint,rst);
}
return rst;
}
private void writeBack(JoinPoint joinPoint,Object rst) throws IllegalAccessException{
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("parm",rst);
SpelExpressionParser parser = new SpelExpressionParser();
Expression exp = null;
//获取某个类的自身的所有字段,不包括父类的字段。
var fields = rst.getClass().getDeclaredFields();
for (var field : fields) {
var annotation = field.getAnnotation(UserAttrDependence.class);
if(annotation!=null){
//解析SpEL表达式,获取属性依赖值
var spelExpresion = annotation.value();
exp = parser.parseExpression(spelExpresion);
String id = (String) exp.getValue(context);
//回写属性值
field.setAccessible(true);
field.set(rst,userInfoService.getDisplayName(id));
}
}
}
五、完整代码
1. 自定义注解@UserAttrDependence
标记字段是需要回显的,value定义回显依赖的id属性。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/***
* 用户属性依赖定义注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserAttrDependence {
/**
* 标准SpEL表达式,标记当前字段回显依赖的ID字段,<br>
* 如: 当前对象user的createName属性,需要依赖createId的值回显,<br>
* 可以写成:@UserAttrDependence("#parm.createId")<br>
* 回显操作查看:{@link UserAttrPlaybackInterceptor}
* @return
*/
String value();
}
2. 自定义注解@UserAttrPlayback
标记方法需要对返回值进行重写。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/***
* 用户信息回显注解,用于在方法上标记,表示该方法需要回显用户信息
*/
@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserAttrPlayback {
}
3. 拦截器UserAttrPlaybackInterceptor
拦截标记@UserAttrPlayback注解的方法,对返对象类型属性上标记@UserAttrDependence注解的属性,值进行重写操作。
import com.example.service.UserInfoService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/***
* @description 用户属性回写切面
*/
@Aspect
@Component
public class UserAttrPlaybackInterceptor {
@Autowired
private UserInfoService userInfoService;
@AfterReturning(returning="rst"
, pointcut="@annotation(com.example.aspect.userinfo.UserAttrPlayback)")
public Object AfterExec(JoinPoint joinPoint,Object rst) throws IllegalAccessException {
if(rst==null)
return rst;
var isList = rst instanceof Collection;
if(isList){
List<Object> list = (List<Object>) rst;
list.forEach(x -> {
try {
writeBack(joinPoint,x);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
}else{
writeBack(joinPoint,rst);
}
return rst;
}
private void writeBack(JoinPoint joinPoint,Object rst) throws IllegalAccessException{
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("parm",rst);
SpelExpressionParser parser = new SpelExpressionParser();
Expression exp = null;
//获取某个类的自身的所有字段,不包括父类的字段。
var fields = rst.getClass().getDeclaredFields();
for (var field : fields) {
var annotation = field.getAnnotation(UserAttrDependence.class);
if(annotation!=null){
//解析SpEL表达式,获取属性依赖值
var spelExpresion = annotation.value();
exp = parser.parseExpression(spelExpresion);
String id = (String) exp.getValue(context);
//回写属性值
field.setAccessible(true);
field.set(rst,userInfoService.getDisplayName(id));
}
}
}
// @Pointcut("@annotation(com.example.aspect.userinfo.UserInfoWriteBack)")
// public void point(){};
//
//
// @Before("point()")
// public void before(JoinPoint joinPoint) throws Exception{
// System.out.println("~~~ before");
// }
// @After("point()")
// public void after(JoinPoint joinPoint){
// System.out.println("~~~ after");
// }
//
// @Around("point()")
// public Object around(ProceedingJoinPoint pjp) throws Throwable{
// System.out.println("~~~ around begin");
// Object rst = pjp.proceed(); //运行doSth(),返回值用一个Object类型来接收
// System.out.println("~~~ around end");
// return rst;
// }
}
4. 在表实体类上标记需要回显的属性
万事俱备,不管什么表实体只需要在属性上标记个@UserAttrDependence注解,就可以自动完成userId到userName的映射了。
@TableName标记库表,@TableField标记次属性在表中不存在。
@TableName(value = "user_info", schema="mydb")
public class UserInfo{
private String userId;
@UserAttrDependence("#parm.userId") //自定义标记需要回显的属性和依赖的属性;
@TableField(exist = false) //标记该字段在库表中不存在
private String displayName;
}
总结
功能实现本身较为直观简单,但同时也存在许多优化空间。例如,在查询用户信息这一环节,我们可以通过引入缓存机制来提高查询效率。在AOP(面向切面编程)的应用上,由于不经常使用,每次都需要重新梳理定义和通知的细节。在此次返回值修改的通知选择过程中,我曾在@Around和@AfterReturning通知之间犹豫,因为两者均允许对返回值进行修改。最终,我选择了之前未曾使用过的@AfterReturning注解,这不仅因为它能够修改返回值,还因为它集成了切入点定义,从而减少了一个方法的编写。
在实现过程中,我也遇到了一些挑战。例如,有些方法返回的是单个对象,而有些则返回集合,这就需要设计一个通用的拦截方式来处理不同类型的返回值。另外,对于私有属性,由于它们被声明为private,因此需要通过反射机制来修改其值,这也增加了实现的复杂性。
有句话说得好,“理论上可行,就一定能行”。一旦逻辑思路清晰,实现起来便顺理成章。沿着这个思路,实现就是水到渠成的事儿了。