目录
一、需求说明
为了简化代码,提示可读性需要针对优先读缓存,如果缓存没有则插入数据库然后再将数据写入缓存并返回。
根据这个需求我们可以基于Spring AOP 去实现。但是有一个问题,缓存的key是一个动态值,对于不同入参它的key是不同的。我们可以使用SpringEL 表达式去解决这个问题。
预期的效果是:
@Cache(prefix = "test", value = "#bizId", expire = CacheTime.CACHE_EXP_DAY) public Object queryFromCache(Long bizId)
二、Spring AOP
首先我们先了解一下Spring AOP的基础知识
2.1 AOP切点指示器
AspectJ 指示器 | 描述 |
args() | 限制连接点匹配参数为执行类型的执行方法 |
@args() | 限制连接点匹配参数由执行注解标注的执行方法 |
execution() | 匹配连接点的执行方法 |
this() | 限制连接点匹配AOP代理的Bean引用类型为指定类型的Bean |
target() | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配目标对象被指定的注解标注的类 |
within() | 限制连接点匹配匹配指定的类型 |
@within() | 限制连接点匹配指定注解标注的类型 |
@annotation | 限制匹配带有指定注解的连接点 |
Spring AOP 中常用的是:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)
throws-pattern?)
2.2 AOP表达式
表达式 | 说明 |
execution(“* *.*(..)”) | 匹配所有 |
execution(“* *.set*(..)) | 匹配所有以set开头的方法 |
execution(“* com.david.biz.service.impl.*(..)) | 匹配指定包下所有的方法 |
execution(“* com.david..*(..)”) | 匹配指定包以及其子包下的所有方法 |
execution(“* com.david..*(java.lang.String)) | 匹配指定包以及其子包下 参数类型为String 的方法 |
2.3、Spring AOP通知类型
通知类型 | 说明 |
前置通知(Before) |
在目标方法被调用之前调用通知功能。 |
后置通知(After) |
在目标方法完成之后调用通知,此时不会关心方法的输出是什么 |
返回通知(After-returning) |
在目标方法成功执行之后调用通知。 |
异常通知(After-throwing) |
目标方法抛出异常后调用通知。 |
环绕通知(Around) |
通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。 |
2.4、示例代码
/**
* execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)
throws-pattern?)
*arg() 限制连接点匹配参数为指定类型的执行方法
*@args() 限制连接点匹配参数由执行注解标注的执行
*execution() 用于匹配连接点的执行方法
*this() 限制连接点匹配AOP代理的Bean引用为执行类型的类
*target() 限制连接点匹配目标对象为指定类型的类
*@target() 限制连接点匹配特定的执行对象,这些对象应具备指定的注解类型
*@annotation()限制匹配带有指定注解的连接点
*
*
*
* @author zhangwei_david
* @version $Id: LogAspect.java, v 0.1 2014年11月29日 下午1:10:13 zhangwei_david Exp $
*/
@Component
@Aspect
public class LogAspect {
private static final Logger logger = LogManager.getLogger(LogAspect.class);
/**
* 匹配参数是任何类型,任何数量 且在com,david.biz包或子包下的方法
*/
@Pointcut("args(..)&&within(com.david.biz..*)")
public void arg() {
}
@Pointcut("@args(com.david.aop.LoggingRequired)")
public void annotationArgs() {
}
@Pointcut("@annotation(com.david.aop.LoggingRequired)")
public void logRequiredPointcut() {
}
@Pointcut("args(java.lang.String,*)")
public void argsWithString() {
}
@Pointcut("target(com.david.biz.service.impl.BookServiceImpl)")
public void targetPointcut() {
}
@Pointcut("@target(org.springframework.stereotype.Service)")
public void targetAnnotation() {
}
// @Around("execution(* org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(..))")
// public Object aa(ProceedingJoinPoint pjp) throws Throwable {
// try {
// Object retVal = pjp.proceed();
// System.out.println(retVal);
// return retVal;
// } catch (Exception e) {
// System.out.println("异常");
// return null;
// }
// }
@Before(value = "logRequiredPointcut()")
public void before(JoinPoint joinPoint) {
LogUtils.info(logger,
" 连接点表达式@annotation(com.david.aop.LoggingRequired) - method={0} has been visited",
joinPoint.getSignature().getName());
}
@Before(value = "arg()")
public void beforeArg(JoinPoint joinPoint) {
LogUtils.info(logger,
"连接点表达式:args(..)&&within(com.david.biz..*) method ={0}, args ={1},target={2}",
joinPoint.getSignature().getName(), ToStringBuilder.reflectionToString(
joinPoint.getArgs(), ToStringStyle.SHORT_PREFIX_STYLE), joinPoint.getTarget()
.getClass().getName());
}
@Before(value = "argsWithString()")
public void beforeArgWithString(JoinPoint joinPoint) {
LogUtils.info(logger, "连接点表达式:args(java.lang.String,*) method={0} ,args ={1},target={2}",
joinPoint.getSignature().getName(), ToStringBuilder.reflectionToString(
joinPoint.getArgs(), ToStringStyle.SHORT_PREFIX_STYLE), joinPoint.getTarget()
.getClass().getName());
}
@Before(value = "annotationArgs()")
public void beforeAnnotationArgs(JoinPoint joinPoint) {
LogUtils
.info(
logger,
"连接点表达式:@args(com.david.annotation.validate.Length,*) method={0} ,args ={1},target={2}",
joinPoint.getSignature().getName(), ToStringBuilder.reflectionToString(
joinPoint.getArgs(), ToStringStyle.SHORT_PREFIX_STYLE), joinPoint.getTarget()
.getClass().getName());
}
@Before(value = "targetPointcut()")
public void beforeTarget(JoinPoint joinPoint) {
LogUtils
.info(
logger,
"连接点表达式:target(com.david.biz.service.impl.BookServiceImpl) method={0} ,args ={1},target={2}",
joinPoint.getSignature().getName(), ToStringBuilder.reflectionToString(
joinPoint.getArgs(), ToStringStyle.SHORT_PREFIX_STYLE), joinPoint.getTarget()
.getClass().getName());
}
@Before(value = " targetAnnotation()")
public void beforeTargetAnnotation(JoinPoint joinPoint) {
LogUtils
.info(
logger,
"连接点表达式:@target(org.springframework.stereotype.Service) method={0} ,args ={1},target={2}",
joinPoint.getSignature().getName(), ToStringBuilder.reflectionToString(
joinPoint.getArgs(), ToStringStyle.SHORT_PREFIX_STYLE), joinPoint.getTarget()
.getClass().getName());
}
}
三、需求实现
根据需求我们需要使用注解指示器,也就是基于注解实现。
3.1、定义注解
作为一个缓存注解首先就是缓存key的构成,其次就是缓存的有效期。下面我们就看看源码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Cache {
/**
* 前缀,默认为空
*
* @return
*/
String prefix() default "";
/**
* key的构成数据
*
* @return
*/
String[] value() default {};
/**
* 分割符
*
* @return
*/
String separator() default "_";
/**
* 过期时间,单位为秒,默认为1天
* @return
*/
int expire() default CacheTime.CACHE_EXP_DAY;
}
3.2、定义切面
3.2.1获取方法参数名称
可以通过ASM提供的通过字节码获取方法的参数名称,spring已经集成了相关功能,我们可以方便的使用它。
第一步、声明一个成员并初始化变量LocalVariableTableParameterNameDiscoverer
private LocalVariableTableParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
第二步、通过反射方式获取方法参数类型
Class[] argTypes = ((MethodSignature) pjp.getSignature()).getMethod().getParameterTypes();
第三步、根据方法名称和参数类型获取方法
Method method = pjp.getTarget().getClass().getMethod(pjp.getSignature().getName(), argTypes);
第四步、获取方法的参数名称
String[] paraNameArr = nameDiscoverer.getParameterNames(method);
3.2.2 构建缓存Key
第一步、声明并初始化成员变量ExpressionParser
private ExpressionParser parser = new SpelExpressionParser();
第二步、构建SpringEL上下文
从需求可以知道上下文中就是参数名称和参数值的映射关系
String[] paraNameArr = nameDiscoverer.getParameterNames(method);
Object[] args = pjp.getArgs();
//SPEL上下文
StandardEvaluationContext context = new StandardEvaluationContext(); //把方法参数放入SPEL上下文中
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
第三步、解析构建缓存key
StringBuilder sb = new StringBuilder();
for (String expressionString : soloCache.value()) {
sb.append(cache.separator());
Object temp = parser.parseExpression(expressionString).getValue(context);
String str = temp == null ? "null" : temp.toString();
sb.append(str);
}
return sb.toString();
3.2.3环绕切面处理
环绕切面处理核心流程就是首先从缓存中获取数据,如果有就直接返回,如果没有就执行原始方法并有效数据写入到缓存中。
Object object = cacheClient.get(cache.prefix(), key);
if (object != null) {
return object;
} else {
Object value = pjp.proceed();
if (value != null) {
cacheClient.set(soloCache.prefix(), key, value, soloCache.expire());
}
return value;
}
四、源码
@Aspect
@Component
public class SoloCacheAdvice {
private ExpressionParser parser = new SpelExpressionParser();
@Resource
private CacheClient cacheClient;
private LocalVariableTableParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
@Around(value = "@annotation(cache)")
public Object invoke(ProceedingJoinPoint pjp, Cache cache) throws Throwable {
Method method = getMethod(pjp);
//获取被拦截方法参数名列表(使用Spring支持类库)
String[] paraNameArr = nameDiscoverer.getParameterNames(method);
Object[] args = pjp.getArgs();
//SPEL上下文
StandardEvaluationContext context = new StandardEvaluationContext(); //把方法参数放入SPEL上下文中
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
String key = createKey(cache, context);
Object object = cacheClient.get(soloCache.prefix(), key);
if (object != null) {
return object;
} else {
Object value = pjp.proceed();
if (value != null) {
try {
cacheClient.set(cache.prefix(), key, value, cache.expire());
} catch (Exception e) {
//error Log
}
}
return value;
}
}
private String createKey(Cache cache, StandardEvaluationContext context) {
StringBuilder sb = new StringBuilder();
for (String expressionString : cache.value()) {
sb.append(cache.separator());
Object temp = parser.parseExpression(expressionString).getValue(context);
String str = temp == null ? "null" : temp.toString();
sb.append(str);
}
return sb.toString();
}
public Method getMethod(ProceedingJoinPoint pjp) {
//获取参数的类型
Class[] argTypes = ((MethodSignature) pjp.getSignature()).getMethod().getParameterTypes();
Method method = null;
try {
method = pjp.getTarget().getClass().getMethod(pjp.getSignature().getName(), argTypes);
} catch (NoSuchMethodException | SecurityException e) {
//error Log
}
return method;
}
}
五、其他问题
5.1为什么无法获取到参数名称
首先检查一下maven配置是否配置了<parameters>true</parameters>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<showWarnings>false</showWarnings>
<showDeprecation>false</showDeprecation>
<parameters>true</parameters>
</configuration>
</plugin>