Java - 使用AOP+SpEL基于DB中的用户ID自动补全用户姓名

server/2024/10/19 7:16:09/

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 {@Autowiredprivate 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.ppmy.cn/server/132982.html

相关文章

计算机毕业设计 | SpringBoot大型旅游网站 旅行后台管理系统(附源码)

1&#xff0c; 概述 1.1 项目背景 随着互联网技术的快速发展和普及&#xff0c;旅游行业逐渐转向线上&#xff0c;越来越多的游客选择在线预订旅游产品。传统的线下旅行社模式已不能满足市场需求&#xff0c;因此&#xff0c;开发一个高效、便捷的旅游网站成为行业的迫切需求…

查缺补漏----三次握手与四次挥手

注意事项&#xff1a; ① 如果是和FTP服务器建立连接&#xff0c;那么要建立两个TCP连接。一个是控制连接一个是数据连接。 ② SYN报文段不能携带数据。三次握手的最后一个报文段可以捎带数据&#xff0c;但是如果不携带数据&#xff0c;那么就不消耗序号。 ③ 在断开连接过程中…

jmeter在beanshell中使用props.put()方法的注意事项

在jmeter中&#xff0c;通常使用beanshell去处理一些属性的设置和获取的操作&#xff0c;而这些操作也是有一定的规则的。 1. 设置属性时&#xff0c;在属性名上要加双引号&#xff0c;这代表它不是一个需要用var去声明的变量 这种设置属性的方式才是有效可行的&#xff0c;在…

Parameter-Efficient Fine-Tuning for Large Models: A Comprehensive Survey阅读笔记

Parameter-Efficient Fine-Tuning for Large Models: A Comprehensive Survey 综述阅读笔记 仅记录个人比较感兴趣的部分 基本知识 PEFT的三种分类&#xff1a;additive, selective, reparameterized, and hybrid fine-tuning selective fine-tuning 不需要任何额外的参数&am…

线性代数基础知识

行列式基础知识 一、行列式的定义 行列式是一个函数&#xff0c;其定义域为det的矩阵A&#xff0c;取值为一个标量&#xff0c;写作det(A)或 | A |。行列式可以看作是一般欧氏空间中有向面积或体积概念的推广。在n维欧氏空间中&#xff0c;行列式描述了一个线性变换对“体积”…

本地部署 Milvus

本地部署 Milvus 1. Install Milvus in Docker2. Install Attu, an open-source GUI tool 1. Install Milvus in Docker curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh -o standalone_embed.shbash standalone_embed.sh …

TemporalBench:一个专注于细粒度时间理解的多模态视频理解的新基准。

2024-10-15&#xff0c;由威斯康星大学麦迪逊分校、微软研究院雷德蒙德等机构联合创建了TemporalBench&#xff0c;它通过大约10K个视频问答对&#xff0c;提供了一个独特的测试平台&#xff0c;用以评估各种时间理解和推理能力&#xff0c;如动作频率、运动幅度、事件顺序等。…

jmeter 从多个固定字符串中随机取一个值的方法

1、先新增用户参数&#xff0c;将固定值设置为不同的变量 2、使用下面的函数&#xff0c;调用这写变量 ${__RandomFromMultipleVars(noticeType1|noticeType2|noticeType3|noticeType4|noticeType5)} 3、每次请求就是随机取的值了