我想解决的问题是什么
现在,我正在尝试解决一些通用问题。我们有很多愿意的方法,没有副作用的方法,并为同一参数返回相同的结果。
此外,我们还有一些读取数据的方法,但是数据很少更改。
我们可以使用,并且我们正在使用缓存来改善这种情况下的响应时间。
问题是所有这些方法都是不同的,以及我们使用缓存的方式(从本地值持有人和哈希地图到EHCACHE)。
因此,我们想统一一种缓存方法,以配置不同的缓存,它们的驱逐以及能力策略以及我们如何存储它们的方式。
另外,我不想用额外的支持代码来污染业务代码。
请在没有缓存的情况下进行比较:
public Value get() {
return readValue();
}
和缓存:
public Value get() {
var value = cache.get("value");
if (value != null) {
return value;
}
value = readValue();
cache.put("value", value);
return value
}
我之前做过的更改
首先,我统一的缓存界面并在已经知道的地方替换了:
public interface Cache<K, V> {
V get(K key);
void put(K key, V value);
}
public final class CacheManager {
public static <K, V> Cache<K, V> getInstance(String region) {
...
}
}
现在带有缓存的代码看起来更丑陋:
public Value get() {
var cache = CacheManager.getInstance("region");
var value = cache.get("value");
if (value != null) {
return value;
}
value = readValue();
cache.put("value", value);
return value
}
幸运的是,人类发明了AOP解决此类任务。
面向方面的编程概述
aop允许您将操作(称为Advice
)添加到由表达式指定(称为Pointcut
)的应用程序(称为Join points
)中的某些点。
所有这些东西一起称为Aspect
。
所以在几个步骤中:
- 您有一种要改进的方法。这是你的
Join point
package com.example.apects;
public class Example {
public int calculateTwoPlusTwo() {
return 4;
}
}
- 写你想做的事情。这是你的
Advice
public class ExampleAspect {
public int returnFive() {
return 5;
}
}
- 指定您要在哪里应用它。是你
Pointcut
@Aspect
public class ExampleAspect {
@Pointcut("execution(public int com.example.apects.Example.calculateTwoPlusTwo())")
public void returnFivePointcut() {
}
...
}
- 将所有绑定在一起。现在你有
Aspect
@Aspect
public class ExampleAspect {
@Pointcut("execution(public int com.example.apects.Example.calculateTwoPlusTwo())")
public void returnFivePointcut() {
}
@Around("returnFivePointcut()")
public Object returnFive() {
return 5;
}
}
为缓存问题应用方面
如您所见,我需要知道并指定我想用缓存包裹的所有方法。
坏消息,我可以用来减少点数数的方法名称中没有模式。
好消息,Java具有可能与之合作的注释和方面。
所以我可以做类似的事情:
- 创建自定义注释
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
String value() default "common";
}
- 写方面
@Aspect
public class CachingAspect {
@Pointcut("@annotation(cacheable)")
public void cachingAnnotation(Cacheable cacheable) {}
@Around("cachingAnnotation(cacheable)")
public Object checkCache(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
var cache = CacheManager.getInstance(cachable.value());
var value = cache.get("value");
if (value != null) {
return value;
}
value = pjp.proceed();
cache.put("value", value);
return value;
}
}
- 对目标方法进行注释:
@Cacheble("region")
public Value get() {
return readValue();
}
看起来不错。我的缓存与业务代码分开,但是:
- 我需要解决缓存密钥的问题
- 某种程度上逻辑应取决于方法名称和参数
- 我是否没有任何线索
对于第一期,我将使用完整的类和目标方法名称和args作为数组。
我需要覆盖equals\hashCode
进行争论,并且可以排除一些论点。但是我稍后会关心它。
@Aspect
public class CachingAspect {
...
@Around("cachingAnnotation(cacheable)")
public Object checkCache(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
var cache = CacheManager.getInstance(cacheable.value());
Signature signature = pjp.getSignature();
if (!(signature instanceof MethodSignature)) {
return pjp.proceed();
}
MethodSignature methodSignature = (MethodSignature)signature;
Method method = methodSignature.getMethod();
CacheKey key = new CacheKey(
method.getDeclaringClass().getCanonicalName(),
method.getName(),
pjp.getArgs()
);
var value = cache.get(key);
if (value != null) {
return value;
}
value = pjp.proceed();
cache.put(key, value);
return value;
}
public static class CacheKey {
private final String className;
private final String methodName;
private final Object[] args;
public CacheKey(String className, String methodName, Object[] args) {
this.className = className;
this.methodName = methodName;
this.args = args;
}
@Override
public boolean equals(Object o) {
...
}
@Override
public int hashCode() {
...
}
}
}
关于第二期,我将使用facteactj maven插件进行编译时间编织。
该插件具有showWeaveInfo
选项,可以记录有关编织的内容以及如何编织的所有信息。
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</version>
<configuration>
<complianceLevel>11</complianceLevel>
<source>11</source>
<target>11</target>
<showWeaveInfo>true</showWeaveInfo>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
构建完成后,我可以检查日志
[INFO] Join point 'method-call(com.example.apects.Example$Value com.example.apects.Example.getValue())' in Type 'com.example.apects.Example' (Example.java:7) advised by around advice from 'com.example.apects.CachingAspect' (CachingAspect.java:22)
[INFO] Join point 'method-execution(com.example.apects.Example$Value com.example.apects.Example.getValue())' in Type 'com.example.apects.Example' (Example.java:12) advised by around advice from 'com.example.apects.CachingAspect' (CachingAspect.java:22)
好吧,在这里我可能会看到我的加入点已被告知两次,因此我的代码也将被称为两次。
- 方法调用点(
method-call
)中的第一次 - 方法(
method-execution
)第二次
因此,我想修改仅在执行中使用的点键,因为我想在项目中仅缓存方法。
如果我在第三方库中缓存一些方法,我需要使用method-call
建议将与缓存相关的代码围绕方法调用。
因此,我将添加execution(* *.*(..))
中的建议。您可以这样读取它:在任何类别中使用任何参数和任何修改器的任何类中执行任何方法。
@Around(value = "execution(* *.*(..)) && cachingAnnotation(cacheable)", argNames = "pjp,cacheable")
public Object checkCache(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
...
}
重建并检查日志:
[INFO] Join point 'method-execution(com.example.apects.Example$Value com.example.apects.Example.getValue())' in Type 'com.example.apects.Example' (Example.java:12) advised by around advice from 'com.example.apects.CachingAspect' (CachingAspect.java:22)