5

【基础系列】Spring之AOP结合SpEL实现日志输出的注意事项

 3 years ago
source link: https://spring.hhui.top/spring-blog/2021/06/08/210608-SpringBoot%E5%9F%BA%E7%A1%80%E7%AF%87AOP%E7%BB%93%E5%90%88SpEL%E5%AE%9E%E7%8E%B0%E6%97%A5%E5%BF%97%E8%BE%93%E5%87%BA%E7%9A%84%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
logo.jpg

【基础系列】Spring之AOP结合SpEL实现日志输出的注意事项

使用AOP来打印日志大家一把都很熟悉了,最近在使用的过程中,发现了几个有意思的问题,一个是SpEL的解析,一个是参数的JSON格式输出

I. 项目环境

1. 项目依赖

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

开一个web服务用于测试

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

II. AOP & SpEL

关于AOP与SpEL的知识点,之前都有过专门的介绍,这里做一个聚合,一个非常简单的日志输出切面,在需要打印日志的方法上,添加注解@Log,这个注解中定义一个key,作为日志输出的标记;key支持SpEL表达式

1. AOP切面

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String key();
}
@Slf4j
@Aspect
@Component
public class AopAspect implements ApplicationContextAware {
private ExpressionParser parser = new SpelExpressionParser();
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

@Around("@annotation(logAno)")
public Object around(ProceedingJoinPoint joinPoint, Log logAno) throws Throwable {
long start = System.currentTimeMillis();
String key = loadKey(logAno.key(), joinPoint);
try {
return joinPoint.proceed();
} finally {
log.info("key: {}, args: {}, cost: {}", key,
JSONObject.toJSONString(joinPoint.getArgs()),
System.currentTimeMillis() - start);
}
}

private String loadKey(String key, ProceedingJoinPoint joinPoint) {
if (key == null) {
return key;
}

StandardEvaluationContext context = new StandardEvaluationContext();

context.setBeanResolver(new BeanFactoryResolver(applicationContext));
String[] params = parameterNameDiscoverer.getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
context.setVariable(params[i], args[i]);
}

return parser.parseExpression(key).getValue(context, String.class);
}

private ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

上面这个逻辑比较简单,和大家熟知的使用姿势没有太大的区别

2. StandardEvaluationContext安全问题

关于StandardEvaluationContext的注入问题,有兴趣的可以查询一下相关文章;对于安全校验较高的,要求只能使用SimpleEvaluationContext,使用它的话,SpEL的能力就被限制了

如加一个测试

@Data
@Accessors(chain = true)
public class DemoDo {

private String name;

private Integer age;
}
@Service
public class HelloService {

@Log(key = "#demo.getName()")
public String say(DemoDo demo, String prefix) {
return prefix + ":" + demo;
}
}

为了验证SimpleEvaluationContext,我们修改一下上面的loadKeys方法

private String loadKey(String key, ProceedingJoinPoint joinPoint) {
if (key == null) {
return key;
}

SimpleEvaluationContext context = new SimpleEvaluationContext.Builder().build();
String[] params = parameterNameDiscoverer.getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
context.setVariable(params[i], args[i]);
}

return parser.parseExpression(key).getValue(context, String.class);
}
@SpringBootApplication
public class Application {

public Application(HelloService helloService) {
helloService.say(new DemoDo().setName("一灰灰blog").setAge(18), "welcome");
}

public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

直接提示方法找不到!!!

3. gson序列化问题

上面的case中,使用的FastJson对传参进行序列化,接下来我们采用Gson来做序列化

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>

然后新增一个特殊的方法

@Service
public class HelloService {
/**
* 字面量,注意用单引号包裹起来
* @param key
* @return
*/
@Log(key = "'yihuihuiblog'")
public String hello(String key, HelloService helloService) {
return key + "_" + helloService.say(new DemoDo().setName(key).setAge(10), "prefix");
}
}

注意上面方法的第二个参数,非常有意思的是,传参是自己的实例;再次执行

public Application(HelloService helloService) {
helloService.say(new DemoDo().setName("一灰灰blog").setAge(18), "welcome");

String ans = helloService.hello("一灰灰", helloService);
System.out.println(ans);
}

直接抛了异常

这就很尴尬了,一个输出日志的辅助工具,因为序列化直接导致接口不可用,这就不优雅了;而我们作为日志输出的切面,又是没有办法控制这个传参的,没办法要求使用的参数,一定能序列化,这里需要额外注意 (比较好的方式就是简单对象都实现toString,然后输出toString的结果;而不是json串)

虽然上面一大串的内容,总结下来,也就两点

  • SpEL若采用的是SimpleEvaluationContext,那么注意spel的功能是减弱的,一些特性不支持
  • 若将方法参数json序列化输出,那么需要注意某些类在序列化的过程中,可能会抛异常

(看到这里的小伙伴,不妨点个赞,顺手关注下微信公众号”一灰灰blog“,我的公众号已经寂寞的长草了😭)

III. 不能错过的源码和相关知识点

AOP系列博文

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

打赏 如果觉得我的文章对您有帮助,请随意打赏。

分享到


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK