当前位置: 首页 > article >正文

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,因此需要通过反射机制来修改其值,这也增加了实现的复杂性。

有句话说得好,“理论上可行,就一定能行”。一旦逻辑思路清晰,实现起来便顺理成章。沿着这个思路,实现就是水到渠成的事儿了。


http://www.kler.cn/news/355582.html

相关文章:

  • 【网络安全】盲SSRF+CSP绕过实现XSS
  • 使用ROS资源编排一键部署LNMP建站环境,手动整理教程
  • @PostConstruct和afterPropertiesSet方法执行多次的原因
  • DirectX 11 和 Direct3D 11 的关系
  • WordPress官方发布“新”插件“SCF”(安全自定义字段)
  • 【C++基础篇】——逐步了解C++
  • 【ROS2】订阅手柄数据,发布运动命令
  • 小程序如何根据用户的不同显示不同导航栏
  • Docker可视化管理工具DockerUI的使用
  • go压缩的使用
  • axios的使用
  • Java基础概览和常用知识(九)
  • 鸿蒙网络编程系列11-使用HttpRequest上传文件到服务端示例
  • Flutter项目打包ios, Xcode 发布报错 Module‘flutter barcode_scanner‘not found
  • LabVIEW提高开发效率技巧----减少UI更新频率
  • Python知识点:基于Python技术,如何使用AirSim进行无人机模拟
  • 1.docker-compose
  • ubuntu下安装图片编辑工具shutter
  • 数据可视化-使用python制作词云图(附代码)
  • nodejs 实现docker 精简可视化控制