基本概念
Spring表达式语言(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。
虽然SpEL作为Spring产品组合中表达式评估的基础,但并不直接与Spring绑定,可以独立使用。为了自包含,本章中的许多示例将SpEL用作独立表达式语言。
基本使用
最简单的case:
// 创建一个解析器
ExpressionParser parser = new SpelExpressionParser();
// 解析一个表达式
Expression exp = parser.parseExpression("'Hello World'");
// 通过表达式获取值
String message = (String) exp.getValue();
稍微复杂:调用方法
ExpressionParser parser = new SpelExpressionParser();
// 调用字符串的方法
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();
再次进阶:读取外部bean的属性、方法
// 创建并设置日历
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);
// 构造函数参数分别为姓名、生日和国籍。
Inventor tesla = new Inventor("尼古拉·特斯拉", c.getTime(), "塞尔维亚人");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name"); // 将姓名解析为表达式
String name = (String) exp.getValue(tesla);
// name == "尼古拉·特斯拉"
exp = parser.parseExpression("name == '尼古拉·特斯拉'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true
理解 EvaluationContext
EvaluationContext 接口用于在评估表达式时解析属性、方法或字段,并帮助执行类型转换。Spring 提供了两种实现。
SimpleEvaluationContext:公开了一组基本的 SpEL 语言特性和配置选项,适用于不需要 SpEL 语言语法的全部功能并且应该有意义地受限制的表达式类别。示例包括但不限于数据绑定表达式和基于属性的过滤器。
StandardEvaluationContext:公开了完整的 SpEL 语言特性和配置选项。您可以使用它指定默认根对象,并配置每个可用的与评估相关的策略
另外,可以通过StandardEvaluationContext进行变量setVariable、函数的注入registerFunction,实现Spel表达式可以进行读取。
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
MethodType.methodType(String.class, Object[].class));
context.setVariable("message", mh);
// 评估为 "Simple message: <Hello World>"
String message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')")
.getValue(context, String.class);
实战
依赖
引入基本的依赖
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
代码实现
日志注解
@Target(ElementType.METHOD)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value();
}
切面实现
@Component
@Aspect
public class EvaluationContextAOP {
private final Logger logger = LoggerFactory.getLogger(EvaluationContextAOP.class);
private final DefaultParameterNameDiscoverer nameDiscovery = new DefaultParameterNameDiscoverer();
private final ExpressionParser parser = new SpelExpressionParser();
// 表达式缓存
private final Map<Method, Expression> expressionCache = new ConcurrentHashMap<>();
// 函数缓存
private final Map<String, Method> functionCache = new ConcurrentHashMap<>();
@Around("@annotation(cn.mj.springboottest.log.Log)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
boolean skip = false;
Object target = joinPoint.getTarget();
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
Class<?>[] argTypes = new Class[args.length];
for (int i = 0; i < args.length; i++) {
argTypes[i] = args[i].getClass();
}
Method method = ReflectionUtils.findMethod(target.getClass(), methodName, argTypes);
Log annotation = AnnotationUtils.findAnnotation(method, Log.class);
String expr = annotation.value();
Expression expression = null;
if (expr.isEmpty()){
skip = true;
}else{
expression = expressionCache.computeIfAbsent(method, k -> parser.parseExpression(expr));
}
Object result = null;
Throwable throwable = null;
try {
result = joinPoint.proceed();
}catch (Throwable e) {
throwable = e;
throw e;
}finally {
if (!skip){
StandardEvaluationContext context = new MethodBasedEvaluationContext(target, method, args, nameDiscovery);
// 注册函数可以进行使用
// context.registerFunction();
context.setVariable("result",result);
String logContent = expression.getValue(context, String.class);
logger.info(logContent);
}
}
return result;
}
}
使用注解进行标记
@RestController
@Validated
public class TestController {
@Resource
TestService testService;
@PostMapping("/test")
@Log("'测试:name is ' + #testBean.name")
public ResponseEntity<String> test(@Validated @RequestBody TestBean testBean,
@RequestParam("id") @NotNull(message = "id不能为空") @Min(value = 1,message = "id必须大于1") Long id)
{
testService.test(new TestBean("",1));
return ResponseEntity.ok("ok");
}
@GetMapping("/query")
@Log("'测试:result name is ' + #result.name")
public TestBean query(){
return new TestBean("query",1);
}
}
总结
以上基于AOP+Spel实现了一个丐版的优雅日志记录工具,在实际应用中可能还需要记录其他的信息:日志记录、异常、执行时长、链路日志等等,都可以基于此进行扩展(定义一个日志Bean、使用ThreadLocal等等实现)。