如何更改已经书面的代码:方面
#java #开发日志 #cache #aspects

我想解决的问题是什么

现在,我正在尝试解决一些通用问题。我们有很多愿意的方法,没有副作用的方法,并为同一参数返回相同的结果。
此外,我们还有一些读取数据的方法,但是数据很少更改。

我们可以使用,并且我们正在使用缓存来改善这种情况下的响应时间。
问题是所有这些方法都是不同的,以及我们使用缓存的方式(从本地值持有人和哈希地图到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();
}

看起来不错。我的缓存与业务代码分开,但是:

  1. 我需要解决缓存密钥的问题
    • 某种程度上逻辑应取决于方法名称和参数
  2. 我是否没有任何线索

对于第一期,我将使用完整的类和目标方法名称和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)