【springboot项目】利用AOP+注解实现代码解耦

本文源码地址:https://github.com/nieandsun/NRSC-STUDY

1 一段臃肿的代码

线上运行的项目必然会出现这样或那样的问题,为了让维护者迅速且准确的对问题进行定位,在代码的关键位置打log肯定必不可少。于是很多人的代码都写成了下面的样子:

/***
  * 臃肿的代码
  * @param positionCode
  * @return
  */
 public List<UserInfo> getUserInfoListByPositionCode111(Long positionCode) {
     //(0) 还可能先去缓存里拿数据,如果拿不到再走下面的逻辑
     //(1) 记录日志
     String loginUserName = "james"; //模拟获取当前登陆用户的方法
     log.info("【{}】调用getUserInfoListByPositionCode方法获取信息,参数为:【{}】",
             loginUserName, positionCode);
     //(2) 异常捕捉
     try {
         //(3)真正的业务代码
         List<UserInfo> userInfos = aopRepository.getUserInfoListByPositionCode(positionCode);

         //(3)-1  还可能有将拿到的数据存到缓存等其他操作
         return userInfos;
     } catch (Exception e) {
         //(4) 捕捉到异常后打个日志
         log.error("【{}】调用getUserInfoListByPositionCode方法查询数据库出错,参数为:【{}】",
                 loginUserName, positionCode);
         //(5)将异常抛出交由统一异常处理类处理
         throw e;
     }
 }

这种代码的一些问题:

(1)可读性差 — 本来只是想根据positionCode查一下用户,一看这个方法却这么长,让人根本没有想多看的欲望;
(2) 可复用性差 — 肯定service层的很多方法都需要按照上面的方式进行打log,假如每一个需要的方法都这样写一遍,多累啊;
(3)可维护性差 — 万一有一天日志里,不让出现用户名,那就要对每一个方法进行检查,然后删掉日志中的用户名;
。。。。

2 利用AOP+注解实现代码解耦

2.1自定义注解

package com.nrsc.elegant.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD}) //指定注解在类上、方法上生效
@Retention(RetentionPolicy.RUNTIME) //指定本注解在运行期起作用
public @interface LogAnnotation {
    /***
     *  用于获取方法中的某个参数的值
     * @return
     */
    String key();

    /***
     * 是否记录日志 --- 默认不记录
     * @return
     */
    boolean needLog() default false;
}

2.2 日志切面类开发 — AOP

  • 切面类
package com.nrsc.elegant.aop;
import com.nrsc.elegant.annotation.LogAnnotation;
import com.nrsc.elegant.util.SpelParserUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
@Aspect
@Component
@Slf4j
public class LogAspect {

    @Around("@annotation(logAnnotation)")
    public Object doAround(ProceedingJoinPoint pjp, LogAnnotation logAnnotation) throws Throwable {
        //(0) 获取到key的值可能会有其他用途,比如去缓存里查询数据
        String key = logAnnotation.key();
        //解析注解中el表达式对应的变量的值
        log.info("获取指定的形参的值" + this.getKey(key, pjp));

        boolean needLog = logAnnotation.needLog();
        //(1) 记录日志
        //模拟获取当前登陆用户的方法
        String loginUserName = "james";
        //获取方法名和参数
        String methodName = pjp.getSignature().getName();
        List<Object> args = Arrays.asList(pjp.getArgs());
        if (needLog) {
            log.info("【{}】调用【{}】方法获取信息,参数为:【{}】", loginUserName, methodName, args);
        }
        //(2) 异常捕捉
        try {
            //(3)真正的业务代码
            Object proceed = pjp.proceed();
            //(3)-1  还可能有将拿到的数据存到缓存等其他操作
            return proceed;
        } catch (Throwable throwable) {
            //(4) 捕捉到异常打印一句日志
            log.error("【{}】调用【{}】方法查询数据库失败,参数为:【{}】", loginUserName, methodName, args);
            //(5) 将异常抛出交由统一异常处理类处理
            throw throwable;
        }
    }

    /***
     *  解析获得key中的真实值
     * @param key
     * @param pjp
     * @return
     */
    private String getKey(String key, ProceedingJoinPoint pjp) {
        //从连接点里获取到当前方法
        Method method = ((MethodSignature) (pjp.getSignature())).getMethod();
        //获取方法形参的名字
        String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(method);
        return SpelParserUtil.getKey(key, parameterNames, pjp.getArgs());
    }
}
  • 解析注解中el表达式对应值的工具类
package com.nrsc.elegant.util;

import org.springframework.expression.Expression;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

public class SpelParserUtil {
    private SpelParserUtil() {
    }

    private static ExpressionParser parser = new SpelExpressionParser();

    /***
     * @param key   el表达式字符串,占位符以#开头
     * @param paramsNames 形参名称,可以理解为占位符名称
     * @param args 参数值,可以理解为占位符真实值
     * @return 返回el表达式经过参数替换后的字符串
     */
    public static String getKey(String key, String[] paramsNames, Object[] args) {
        //将key字符串解析为el表达式
        Expression exp = parser.parseExpression(key);
        //初始化赋值上下文
        EvaluationContext context = new StandardEvaluationContext();
        if (args.length <= 0) {
            return null;
        }
        //将 形参 和 形参值 以配对的方式配置到赋值上下文中
        for (int i = 0; i < args.length; i++) {
            context.setVariable(paramsNames[i], args[i]);
        }
        //根据赋值上下文运算el表达式
        return exp.getValue(context, String.class);
    }
}

2.3 臃肿的代码瘦身以后

 @Override
 @LogAnnotation(key = "#positionCode", needLog = true)
 public List<UserInfo> getUserInfoListByPositionCode(Long positionCode) {
     return aopRepository.getUserInfoListByPositionCode(positionCode);
 }

可以发现此时代码已经被大大简化,变得不再臃肿。


3 简单测试

假设

  • AopDemoRepository中对应的getUserInfoListByPositionCode方法如下:
package com.nrsc.elegant.Repository;

import com.nrsc.elegant.pojo.UserInfo;
import org.springframework.stereotype.Repository;

import java.util.Arrays;
import java.util.List;
@Repository
public class AopDemoRepository {

    public List<UserInfo> getUserInfoListByPositionCode(Long positionCode) {
        Long i = 1 / positionCode;
        UserInfo u1 = new UserInfo("小明", 18, "male");
        UserInfo u2 = new UserInfo("小红", 18, "female");
        return Arrays.asList(u1, u2);
    }
}
  • AopDemoController 代码如下:
@RestController
@RequestMapping(value = "/users")
public class AopDemoController {

    @Autowired
    private AopDemoService aopDemoService;


    @GetMapping("/list/{positionCode}")
    public List<UserInfo> getUserInfo(@PathVariable Long positionCode) {
        return aopDemoService.getUserInfoListByPositionCode(positionCode);
    }
}

(1)启动项目,直接在浏览器访问http://localhost:9100/users/list/111 ,获取到正确结果,且打印出相应日志:

在这里插入图片描述

(2) 直接在浏览器访问 http://localhost:9100/users/list/0,可以看到后端正确打印了日志,且打印出了相应的异常信息,可以让维护者快速定位到问题的正确位置,同时前端获取到统一异常处理的结果。

在这里插入图片描述

发布了189 篇原创文章 · 获赞 187 · 访问量 39万+

猜你喜欢

转载自blog.csdn.net/nrsc272420199/article/details/103356050
今日推荐