Статьи

DIY Аннотации

Начиная с Java 5 в Java были аннотации. Я хотел сделать свою собственную аннотацию, чтобы посмотреть, что нужно. Однако я узнал, что это были просто интерфейсы.

Есть загвоздка

Интерфейсы не имеют зубов позади них. Некоторый кусок кода должен реализовать это. Я подумал, что это где резина отправляется в путь, и я действительно нахожу способ сделать это.

Для начала мне нужна цель

Я выбрал одну недавнюю горячую тему — кеширование. Я не хотел реализовывать JSR 109 (JCache), но я также не хотел делать типичный «Hello World». Я выбрал реализацию двух аннотаций, одна без каких-либо параметров и одна с параметром. Мне также был нужен поставщик кэширования. Если бы я собирался сделать это, то мог бы принести настоящую библиотеку кеширования. Кроме того, в соответствии с моей философией дизайна я использую продукты / библиотеки для достижения цели, вместо того, чтобы все крутить дома. После тщательного рассмотрения я выбрал hazelcast в качестве механизма кэширования. Это самый быстрый на рынке и это бесплатно.

Больше решений

После того, как моя цель была выбрана, мне все еще нужно было выяснить, как поставить зубы позади них. После нескольких копаний я нашел два метода:

отражение

Почти каждый раз, когда я использовал рефлексию, мне было жаль делать такой неуклюжий кусок кода. Кроме того, чтобы сделать это так, как мне хотелось бы, мне нужно было бы создать собственную структуру. Похоже, много работы для двух аннотаций.

Аспектно-ориентированное программирование (АОП)

Это было идеально подходит для того, что я хотел сделать. АОП занимается сокращением стандартного кода в одном месте. Это было бы удобно и увязывалось с кэшированием, потому что кэширование делится на следующие шаги:

  1. Проверьте, была ли эта ситуация сделана раньше.
  2. Если так:
    1. получить сохраненный результат
  3. если не:
    1. запустить функцию
    2. сохранить результат
  4. вернуть результат

Это может быть упрощение, но в ореховой скорлупе это правда. Как и во всем, дьявол кроется в деталях.

Тем временем, Вернемся к AOP Ranch

Хотя я знал, что АОП был местом для меня, я мало что знал об этом. Я обнаружил, что Spring имеет библиотеку AOP, а хорошо известная библиотека — AspectJ. AspectJ мне незнаком, и ему нужен движок для работы. Я намного лучше знаком с Spring, поэтому я выбрал его. Пока я копался в Spring AOP, я обнаружил, что мне нужно углубиться в аннотации AspectJ, поэтому я все равно застрял с AspectJ в той или иной форме.

Новые концепции, новый словарь

Написание аспектов не похоже на написание объектов. Это объекты, но на самом деле не так, конечно, нужен новый набор терминов. Те, что я использовал, находятся в документации Spring AOP

Мне действительно нужно было прочитать страницу пару раз, чтобы понять, что говорится. Один настоятельно рекомендуется сделать то же самое, или остальная часть поста будет звучать как бред.

Что делает Pointcut и как его посоветовать

Дизайн pointcut был легким, так как меня интересовали только методы с аннотацией. Необходимым советом был общий совет, потому что мне нужно было иметь возможность обойти вызов метода, если уже был выполнен соответствующий вызов.

Наконец код

Maven Pom.xml

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<?xml version="1.0" encoding="UTF-8"?>
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>com.darylmathison</groupId>
    <artifactId>annotation-implementation</artifactId>
    <version>1.0-SNAPSHOT</version>
 
    <properties>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring.version>4.2.4.RELEASE</spring.version>
    </properties>
 
    <description>
        This project is an example of how to implement an annotation via Spring AOP.
    </description>
 
    <scm>
        <connection>scm:git:https://github.com/darylmathison/annotation-implementation-example.git</connection>
        <developerConnection>scm:git:git@github.com:darylmathison/annotation-implementation-example.git</developerConnection>
    </scm>
 
    <issueManagement>
        <system>GitHub</system>
    </issueManagement>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>${spring.version}</version>
        </dependency>
 
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
 
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.8</version>
        </dependency>
 
        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast</artifactId>
            <version>3.6</version>
        </dependency>
 
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
 
    </dependencies>
 
    <reporting>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-project-info-reports-plugin</artifactId>
                <version>2.7</version>
                <reportSets>
                    <reportSet>
                        <reports>
                            <report>dependencies</report>
                            <report>index</report>
                            <report>project-team</report>
                            <report>issue-tracking</report>
                            <report>scm</report>
                        </reports>
                    </reportSet>
                </reportSets>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-report-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
                <version>2.10.3</version>
                <reportSets>
                    <reportSet>
                        <reports>
                            <report>javadoc</report>
                            <report>test-javadoc</report>
                        </reports>
                    </reportSet>
                </reportSets>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jxr-plugin</artifactId>
                <version>2.5</version>
                <configuration>
                    <linkJavadoc>true</linkJavadoc>
                </configuration>
                <reportSets>
                    <reportSet>
                        <reports>
                            <report>jxr</report>
                            <report>test-jxr</report>
                        </reports>
                    </reportSet>
                </reportSets>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-changelog-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <type>range</type>
                    <range>90</range>
                </configuration>
            </plugin>
        </plugins>
    </reporting>
</project>

Аннотации

CacheMe

Милое имя для аннотации кэширования, верно?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package com.darylmathison.ai.annotation;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
 * Created by Daryl on 2/19/2016.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface CacheMe {
}

CacheMeNow

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
package com.darylmathison.ai.annotation;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
 * Created by Daryl on 2/19/2016.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface CacheMeNow {
    String key();
}

Конфигурация пружины

Я решил использовать конфигурацию на основе Java вместо XML, как я обычно использую для изменения темпа. Аннотация EnableAspectJAutoProxy — это ключ к началу работы Spring AOP. Я был вне себя, пока не прочитал об этой маленькой драгоценности. Иногда это самая легкая вещь, которая горит день.

AppConfig

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.darylmathison.ai.config;
 
import com.darylmathison.ai.cache.CacheAspect;
import com.darylmathison.ai.service.FibonacciService;
import com.darylmathison.ai.service.FibonacciServiceImpl;
import com.hazelcast.config.Config;
import com.hazelcast.config.EvictionPolicy;
import com.hazelcast.config.MapConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
 
import java.util.HashMap;
import java.util.Map;
 
/**
 * Created by Daryl on 2/20/2016.
 */
@Configuration
@ComponentScan(basePackages = "com.darylmathison.ai")
@EnableAspectJAutoProxy
public class AppConfig {
 
    @Bean
    public Map<String, Object> cache() {
        Config config = new Config();
        MapConfig mapConfig = new MapConfig();
        mapConfig.setEvictionPercentage(50);
        mapConfig.setEvictionPolicy(EvictionPolicy.LFU);
        mapConfig.setTimeToLiveSeconds(300);
        Map<String, MapConfig> mapConfigMap = new HashMap<>();
        mapConfigMap.put("cache", mapConfig);
        config.setMapConfigs(mapConfigMap);
 
        HazelcastInstance instance = Hazelcast.newHazelcastInstance(config);
        return instance.getMap("cache");
    }
 
    @Bean
    public FibonacciService fibonacci() {
        return new FibonacciServiceImpl();
    }
 
    @Bean
    public CacheAspect cacheAspect() {
        return new CacheAspect();
    }
}

Сервисный код

Классический дизайн на основе Spring нуждается в сервисе, верно? Поскольку Spring использует прокси для реализации своего AOP, настоятельно рекомендуется определить интерфейс для реализуемого аннотированного класса.

FibonacciService

01
02
03
04
05
06
07
08
09
10
11
package com.darylmathison.ai.service;
 
/**
 * Created by Daryl on 2/20/2016.
 */
public interface FibonacciService {
 
    long calculate(int rounds);
 
    long calculateWithKey(int rounds);
}

FibonacciServiceImpl

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.darylmathison.ai.service;
 
 
import com.darylmathison.ai.annotation.CacheMe;
import com.darylmathison.ai.annotation.CacheMeNow;
 
/**
 * Created by Daryl on 2/20/2016.
 */
public class FibonacciServiceImpl implements FibonacciService {
 
    @Override
    @CacheMe
    public long calculate(int rounds) {
        return sharedCalculate(rounds);
    }
 
    @Override
    @CacheMeNow(key = "now")
    public long calculateWithKey(int rounds) {
        return sharedCalculate(rounds);
    }
 
    private static long sharedCalculate(int rounds) {
        long[] lastTwo = new long[] {1, 1};
 
        for(int i = 0; i < rounds; i++) {
            long last = lastTwo[1];
            lastTwo[1] = lastTwo[0] + lastTwo[1];
            lastTwo[0] = last;
        }
 
        return lastTwo[1];
    }
}

AOP Stuff

Это сердце реализации аннотации. Все остальное — поддержка, чтобы сделать источник, который следует.

SystemArch

Согласно документации Spring, хорошая идея — централизовать определения точек.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package com.darylmathison.ai.cache;
 
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
 
/**
 * Created by Daryl on 2/20/2016.
 */
@Aspect
public class SystemArch {
 
    @Pointcut("@annotation(com.darylmathison.ai.annotation.CacheMe)")
    public void cacheMeCut() {
 
    }
 
    @Pointcut("@annotation(com.darylmathison.ai.annotation.CacheMeNow)")
    public void cacheMeNowCut() {
 
    }
}

CacheAspect

Аннотации Around принимают полные имена методов класса pointcut, чтобы определить, что посоветовать. Рекомендация для аннотации CacheMeNow включает дополнительное условие, позволяющее определить аннотацию, чтобы можно было прочитать ключевой параметр. В CacheMeNow есть ошибка проектирования, которая обнаруживается в тестовом коде.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.darylmathison.ai.cache;
 
import com.darylmathison.ai.annotation.CacheMeNow;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
 
import java.util.Map;
 
/**
 * Created by Daryl on 2/20/2016.
 */
@Aspect
public class CacheAspect {
 
    @Autowired
    private Map<String, Object> cache;
 
    @Around("com.darylmathison.ai.cache.SystemArch.cacheMeCut()")
    public Object simpleCache(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        StringBuffer keyBuffer = new StringBuffer();
        for(Object o: proceedingJoinPoint.getArgs()) {
            keyBuffer.append(o.hashCode());
        }
        String key = keyBuffer.toString();
        Object ret = cache.get(key);
        if(ret == null) {
            ret = proceedingJoinPoint.proceed();
            cache.put(key, ret);
        }
        return ret;
    }
 
    @Around("com.darylmathison.ai.cache.SystemArch.cacheMeNowCut() && @annotation(cacheMeNow)")
    public Object simpleCacheWithParam(ProceedingJoinPoint proceedingJoinPoint, CacheMeNow cacheMeNow) throws Throwable {
        Object ret = cache.get(cacheMeNow.key());
        if(ret == null) {
            ret = proceedingJoinPoint.proceed();
            cache.put(cacheMeNow.key(), ret);
        }
        return ret;
    }
}

Тестовый код

Код драйвера, показывающий, что аннотации вызывают кеширование.

FibonacciTest

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.darylmathison.ai.service;
 
import com.darylmathison.ai.config.AppConfig;
import org.junit.Assert;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
/**
 * Created by Daryl on 2/20/2016.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppConfig.class})
public class FibonacciTest {
 
    private static final int ROUNDS = 12;
    private static final long ANSWER = 377;
 
    @Autowired
    private FibonacciService fibonacci;
 
    @org.junit.Test
    public void testCalculate() throws Exception {
        long start = System.currentTimeMillis();
        Assert.assertEquals(ANSWER, fibonacci.calculate(ROUNDS));
        long middle = System.currentTimeMillis();
        Assert.assertEquals(ANSWER, fibonacci.calculate(ROUNDS));
        long end = System.currentTimeMillis();
        Assert.assertTrue((end - middle) < (middle - start));
    }
 
    @org.junit.Test
    public void testCalculateWithKey() throws Exception {
        Assert.assertEquals(ANSWER, fibonacci.calculateWithKey(ROUNDS));
        // This test should not pass
        Assert.assertEquals(ANSWER, fibonacci.calculateWithKey(13));
    }
}

Вывод

Аннотации не должны быть сложными для реализации. Используя программирование АОП, я смог реализовать две аннотации с небольшим количеством кода.