说明:在一些时候,我们需要在接口介绍到参数前处理参数,像参数校验、参数转换等,本文介绍如何使用AOP来实现此需求。
场景
需求:有一批开放给第三方调用的接口,之前传递的都是用户表的ID,现在需要换成用户表的用户名。如下:
@RestController
@RequestMapping("user")
public class UserController {@Autowiredprivate 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 {@Autowiredprivate 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@DeleteMappingpublic 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;@FieldAnnotationprivate String username;private String password;
}
只传个username
断点打在获取参数后,可以看到id已经补上了,说明切面起作用了。
总结
本文介绍了AOP的一个使用场景,另外使用AOP还可以解决很多问题,像记录接口访问日志、接口鉴权、补全用户信息(类似上面的)。
完整源码:https://github.com/HeZhongYing/aop_use_demo
AOP技术介绍,参考下面这边博客:
- AOP技术