说明:在一些时候,我们需要在接口介绍到参数前处理参数,像参数校验、参数转换等,本文介绍如何使用AOP来实现此需求。
场景
需求:有一批开放给第三方调用的接口,之前传递的都是用户表的ID,现在需要换成用户表的用户名。如下:
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping("{id}")
public User getUser(@PathVariable String id) {
return userMapper.selectUserById(id);
}
}
一般思维,我们可以在响应的接口前,调用一个方法,根据传递的用户名去查询用户表,返回用户ID。这样,所有需要修改的地方,都需要加上这个方法,代码侵入大,不易维护,不优雅。
使用AOP
使用AOP,我们可以考虑在相应的接口上,打上一个自定义注解,表示改接口需要进行处理,然后在对应的参数上,再打上一个注解,表示需要对该参数进行处理,
首先,创建三个注解,如下:
(接口注解,打在接口上,表示需要处理的接口)
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 InterfaceAnnotation {
}
(参数注解,打在参数上,表示需要处理的参数)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamAnnotation {
}
(字段注解,如果参数是对象,打在对象的属性上,表示取出该对象的这个属性处理)
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 FieldAnnotation {
}
重点是切面,切面要做的是取出对应的参数值,进行转换,然后再赋值回去,需要考虑参数是单个字段、对象这两种情况,如下:
import com.hezy.annotation.FieldAnnotation;
import com.hezy.annotation.ParamAnnotation;
import com.hezy.mapper.UserMapper;
import com.hezy.pojo.UserDTO;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@Aspect
@Component
public class UsernameToIdAspect {
@Autowired
private UserMapper userMapper;
@Around("@annotation(com.hezy.annotation.InterfaceAnnotation)")
public Object resolveUsernameToId(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取方法参数列表
Object[] args = joinPoint.getArgs();
// 获取方法参数的注解,这里是二位数组,是因为一个参数可以有多个注解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
// 遍历每个注解
for (int i = 0; i < parameterAnnotations.length; i++) {
// 遍历注解,判断是否有ParamAnnotation注解
for (Annotation annotation : parameterAnnotations[i]) {
// 如果有ParamAnnotation注解
if (annotation instanceof ParamAnnotation) {
// 获取该对象的属性值
String username = (String) args[i];
// 将根据username查询后的accountId赋值给这个参数
args[i] = getUserIdByUsername(username);
}
}
// 参数如果是对象
if (args[i] instanceof UserDTO) {
// 获取对象的所有属性,并遍历
Field[] declaredFields = args[i].getClass().getDeclaredFields();
for (Field field : declaredFields) {
// 如果有 FieldAnnotation 注解
if (field.getAnnotation(FieldAnnotation.class) != null) {
// 设置属性为可访问的
field.setAccessible(true);
// 获取该对象的属性值
String username = (String) field.get(args[i]);
// 将根据username查询后的accountId赋值给该对象的id字段
Field accountId = args[i].getClass().getDeclaredField("id");
accountId.setAccessible(true);
accountId.set(args[i], getUserIdByUsername(username));
}
}
}
}
return joinPoint.proceed(args);
}
/**
* 根据username去查userId
*/
private String getUserIdByUsername(String username) {
String userId = userMapper.selectIdByUsername(username);
if (userId == null) {
throw new RuntimeException("操作失败,该账户不存在");
}
return userId;
}
}
测试
根据username查id
@Select("select id from i_users where username = #{username}")
String selectIdByUsername(@Param("username") String username);
启动项目,传username,可以看到,也能查出数据,说明转换成功了。完全不用去修改原来的代码。
再试下传入一个对象,将对象里面的username字段取出来,然后查出id,赋值给原对象。这样就不影响原来逻辑了。
@InterfaceAnnotation
@DeleteMapping
public void deleteUserByUsername(@RequestBody UserDTO userDTO) {
userMapper.deleteUserByUsername(userDTO);
}
别忘了要在对象属性上打注解
import com.hezy.annotation.FieldAnnotation;
import lombok.Data;
import java.io.Serializable;
@Data
public class UserDTO implements Serializable {
private String id;
@FieldAnnotation
private String username;
private String password;
}
只传个username
断点打在获取参数后,可以看到id已经补上了,说明切面起作用了。
总结
本文介绍了AOP的一个使用场景,另外使用AOP还可以解决很多问题,像记录接口访问日志、接口鉴权、补全用户信息(类似上面的)。
完整源码:https://github.com/HeZhongYing/aop_use_demo
AOP技术介绍,参考下面这边博客:
- AOP技术