上回说到
!
算了。不说了。。。。之前用增强Controller完成了有i18n的支持,后来想想,要不要试试用拦截器和aop各自实现一次试试,说干就干
首先准备了依赖
依赖
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
定义一个切面
我们使用@Aspect
注解在类
上将一个类定义为切面类,
使用@Pointcut(切面表达式)
定义切入点
使用@Around(切入点)
指定一个切面内容切入哪个切入点
方法有一个参数ProceedingJoinPoint pjp
这个就代表被切入的方法,我们可以使用pjp.proceed()使原方法执行,注意这里是使原方法执行,执行到这里就会直接执行原来的方法,如果有返回值可以直接获取;
如果不调用则不会执行原方法内容,调用了原方法之后,就会暂停在等待返回哪个阶段,,所以此时如果使用的使ResponseEntity
响应的,并不会被解析为其中的泛型,返回值依然是ResponseEntity
,不像增强Controller,获取到的object是解析后的泛型所指类型;
响应顺序…方法响应=>aop=>增强controller=>拦截器=>过滤器
完整代码
/**
* 定义一个切面打印请求返回参数信息
*/
@Aspect // 定义为一个切面
@Configuration
public class LogRecordAspect {
private final ResultUtils resultUtils;
public LogRecordAspect(ResultUtils resultUtils) {
this.resultUtils = resultUtils;
}
private static final Logger logger = LoggerFactory.getLogger(LogRecordAspect.class);
// 定义切点Pointcut
@Pointcut("execution(public * com.doria.learnProject.codeStyle.LanguageMapper.*Controller.*(..))")
public void resultPointcut() {
}
/**
* 切面内容,当触发上面的切面以后就会走到这里的代码
*
* @param pjp 方法体
* @return 就是方法返回值
* @throws Throwable
*/
@Around("resultPointcut()") // 指定切面需要插入的切点
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
// 获取当前线程所绑定的requestAttributes
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
// 强转以获取Request
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
// 获取request[其实看到这个demo我也很疑惑为什么不直接拿去注入的HttpServletRequest,但是我很菜,不敢怀疑,听大佬的]
assert sra != null;
HttpServletRequest request = sra.getRequest();
String method = request.getMethod();
String uri = request.getRequestURI();
String json = JSON.toJSONString(request.getParameterMap());
logger.info("收到请求=>Uri:{},请求方式:{},参数:{}", uri, method, json);
// 获取方法的返回值result的值就是返回值
Object result = pjp.proceed();
logger.info("请求结束=>返回值为:" + JSON.toJSONString(result));
// 由于特殊需要,这里需要对参数的返回值进行处理,这里是吧code转为对应的语言填入msg
if (result instanceof Result)
// 调用语言转换方法,大概内容就是强转然后读取code然后通过i18n转换为msg写入msg
return resultUtils.languageFormatToResult(result);
return result;
}
}
语言填充方法
@Autowired
private MessageSource messageSource;
/**
* 语言转换方法
* @param o 待处理对象
* @return 处理后的对象
*/
public Object languageFormatToResult(Object o){
// 由于调用方法前使用了instanceof确定了具体类型,我们这里可以直接强转
Result<?> result = (Result<?>) o;
// 强转以后取出code
String key = result.getCode().toString();
// 设置msg,用code作为key获取参数
result.setMsg(messageSource.getMessage(key,null, LocaleContextHolder.getLocale()));
return result;
}
关于切点和切点表达式
切入点
可以通过这个注解定义一个切入点,其中可以没内容
// 定义切点Pointcut
@Pointcut("execution(public * com.doria.learnProject.codeStyle.LanguageMapper.*Controller.*(..))")
public void resultPointcut() {
}
切点表达式
由于Spring切面粒度最小是达到方法级别,而execution表达式可以用于明确指定方法返回类型,类名,方法名和参数名等与方法相关的部件,并且在Spring中,大部分需要使用AOP的业务场景也只需要达到方法级别即可,因而execution表达式的使用是最为广泛的。如下是execution表达式的语法:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
这里问号表示当前项可以有也可以没有,其中各项的语义如下:
modifiers-pattern:方法的可见性,如public,protected;
ret-type-pattern:方法的返回值类型,如int,void等;
declaring-type-pattern:方法所在类的全路径名,如com.spring.Aspect;
name-pattern:方法名类型,如buisinessService();
param-pattern:方法的参数类型,如java.lang.String;
throws-pattern:方法抛出的异常类型,如java.lang.Exception;
切入点表达式详解
后续补充:
aop遵循一个先进后出原则
time:2021.11.16
可以通过注解@Order(num)
或者实现接口Ordered
重写public int getOrder()
方法控制aop的执行顺序
aop
围绕ProceedingJoinPoint pjp
执行
根据配置的顺序,越小越先执行,执行到pjp.proceed()
后按照顺序倒序再执行之后的
方法一:1234 [pjp] 5678
方法二:abcd [pjp] efg
执行结果是 1234 abcd [pjp] efg 5678
案例:
方法1做redis缓存,方法二取出code根据 语言翻译为msg写入;
原来的顺序是语言翻译先执行,那在pjp执行获取响应结果后,就会先执行最后执行的redis缓存部分,结果就缓存了一个没有翻译过的版本,第二次请求进来发现已经有缓存了直接返回了没有翻译过的版本;
改变顺序,先执行redis缓存aop;
这样redis前段先执行完再执行了翻译部分的前端,pjp执行获取结果后,就会执行到最后执行的翻译部分,翻译完成再交给redis缓存部分。这样redis缓存的就是翻译过的版本。这样到了下一次请求进来发现有缓存直接响应的就是翻译后的版本