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;
}
}
这种代码的一些问题:
2 利用AOP+注解实现代码解耦(1)可读性差 — 本来只是想根据positionCode查一下用户,一看这个方法却这么长,让人根本没有想多看的欲望;
(2) 可复用性差 — 肯定service层的很多方法都需要按照上面的方式进行打log,假如每一个需要的方法都这样写一遍,多累啊;
(3)可维护性差 — 万一有一天日志里,不让出现用户名,那就要对每一个方法进行检查,然后删掉日志中的用户名;
。。。。
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,可以看到后端正确打印了日志,且打印出了相应的异常信息,可以让维护者快速定位到问题的正确位置,同时前端获取到统一异常处理的结果。