前言
哈喽,大家好,今天和大家分享在重构过程中使用注解和反射的一些心得,本章主要包括以下几个问题:
- 在什么场景下使用自定义注解
- 自定义注解的使用方法介绍
- 自定义注解的优缺点
- 反射的使用
1.在什么场景下使用自定义注解
在重构过程中遇到这样一个需求,就是系统会将一部分不易变动的数据存放在redis中,如果用户在界面上修改了这部分数据,就会将新数据重新加载到redis中,这类数据比较多,新增、修改、删除都要对缓存进行更新,旧的代码框架在处理时,是通过硬编码,将更新操作编码在每一个增删改的接口中,根据业务场景不同,分别调用不同的loadRedis方法(如:loadRedis100,loadRedis200)。
代码如下:
less
//缓存更新场景1,每次的增删改都要调用serviceOne的loadRedis方法
@PostMapping("update")
public Result update(@RequestBody UpdateEntity vo) {
//1.业务操作
doBusiness();
//2.更新缓存
serviceOne.loadRedis(vo.getRedisKey());
}
@PostMapping("insert")
public Result add(@RequestBody InsertEntity vo) {
//1.业务操作
doBusiness();
//2.更新缓存
serviceOne.loadRedis(vo.getRedisKey());
}
@PostMapping("delete")
public Result delete(@RequestBody DeleteEntity vo) {
//1.业务操作
doBusiness();
//2.更新缓存
serviceOne.loadRedis(vo.getRedisKey());
}
--------------------------------------------------------------
//缓存更新场景2,每次的增删改都要调用serviceTwo的loadRedis方法
@PostMapping("update")
public Result update(@RequestBody UpdateEntity vo) {
//1.业务操作
doBusiness();
//2.更新缓存
serviceTwo.loadRedis(vo.getRedisKey());
}
@PostMapping("insert")
public Result add(@RequestBody InsertEntity vo) {
//1.业务操作
doBusiness();
//2.更新缓存
serviceTwo.loadRedis(vo.getRedisKey());
}
@PostMapping("delete")
public Result delete(@RequestBody DeleteEntity vo) {
//1.业务操作
doBusiness();
//2.更新缓存
serviceTwo.loadRedis(vo.getRedisKey());
}
如果loadRedis()只是写几处,可能问题还不大,然而我们老系统中涉及到的地方有上百处,如果依旧采取这种方式,无法进行统一管理,对后期的代码维护是非常困难的,这里把开发变成体力活,肯定不是我们希望看到的。
看过面试题的小伙伴肯定一眼就看出来这种情况非常适合aop,我也是这么处理的,首先得理一理思路,明确我们要解决的问题是什么,然后列出解决的思路。
我们要解决的问题:
- 统一管理redis缓存的更新业务
- 减少硬编码
- 动态获取接口入参
2.自定义注解+Aop的示例
- 创建自定义注解
首先定义一个注解,包括两个参数(演示一个静态参数、一个动态参数的获取),customParam 参数是为了从接口入参中获取一些动态数据,比如body中的userName的值,redisKey 则是告诉aop,我需要调用哪个刷新方法(如loadRedis100、loadRedis200)。
less
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RedisLoadAnnotation {
String customParam() default "";
String redisKey() default "";
}
- 使用注解
使用注解时,传入了两个参数,我们后面需要通过反射获取到request对象中的userName的值
less
@PostMapping("test")
@RedisLoadAnnotation(redisKey = "key_animal",customParam = "request.userName")
public void test(String param1 , @RequestBody AnnotationTest request){
logger.info("业务处理");
}
- 定义切面类
接下来定义一个切面类,我使用的是 @AfterReturning,因为我需要在整个业务处理完成并且正常返回才刷新redis。
less
@Slf4j
@Aspect
@Component
public class ReloadRedisAspect {
private static final Logger logger = LoggerFactory.getLogger(ReloadRedisAspect.class);
@Pointcut("@annotation(com.huage.demo.annotation.RedisLoadAnnotation)")
public void redisLoad(){
}
@AfterReturning(pointcut ="redisLoad()")
public void afterReturning(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
//1.获取注解
RedisLoadAnnotation redisLoadAnnotation = method.getAnnotation(RedisLoadAnnotation.class);
String customParamName = redisLoadAnnotation.customParam();
//2.从入参中获取动态数据
String customParamValue = getCustomParam(customParamName, method, joinPoint);
//3.从注解中获取静态参数
String redisKey = redisLoadAnnotation.redisKey();
//4.更新缓存
logger.info("执行更新缓存逻辑");
customService.reloadRedis(customParamValue,redisKey);
}
}
- 获取自定义参数
获取自定义参数我是通过反射进行对比,具体可以看下面的注释
ini
/**
* 通过反射获取自定义参数
**/
private String getCustomParam(String customParamName,Method method,JoinPoint joinPoint){
Parameter[] parameters = method.getParameters();
//获取注解的参数classParam值:即request.userName
String[] customParamArr = customParamName.split("\.");
String instanceName = customParamArr[0];
String paramName = customParamArr[1];
//获取注解配置的参数下标,例如一个方法有多个参数,现在需要找到参数名为即request的参数
Integer index = -1;
for (Parameter parameter : parameters) {
index++;
if(instanceName.equals(parameter.getName())){
break;
}
}
//没有找到注解配置的customParamName对应参数,直接返回
if(index == -1){
return null;
}
//通过反射获取customParamName实际参数值
Object[] args = joinPoint.getArgs();
Object params = args[index];
try {
Field[] declaredFields = params.getClass().getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
if(field.getName().equals(paramName)){
return field.get(params).toString();
}
}
} catch (Exception e) {
logger.error("执行失败");
}
return null;
}
- 更新缓存
在更新缓存时,因为我们有很多业务场景类,每个类中都有一个同名的方法loadRedis,所以我考虑是将这些场景类的包路径维护到数据库中,与redisKey进行映射,这样我们就可以写一个公共方法,通过反射调用不同场景类的loadRedis方法,即: customService.reloadRedis(customParamValue,redisKey) ;
ini
Class clazz = Class.forName(classPath);
Method method = clazz.getDeclaredMethod("loadRedis",Integer.class);
Object obj = clazz.newInstance();
method.invoke(obj,customParamValue);
最后
aop其实大家都很熟悉,也都知道怎么使用,不过之前在开发过程中,遇到类似的场景,有时候为了省事或者说是懒吧,就宁愿去费时费力的复制粘贴,现在我就努力强迫自己,尽量多想一想,就算很简单的技巧,也要动手去做出来,尽量让代码保持干净。
下一篇会继续聊聊重构过程中,遇到的一个有意思的小技巧,如果让代码免于重新打包编译,就能够适应不同的数据源和不同的sql语句。