Java拾遗

Java拾遗:015 - Java注解与自定义注解

2018-08-05  本文已影响23人  ed72fd6aaa3c

Java注解

注解(Annontation)是Java5开始引入的新特征,是那些插入在源码中的程序可读的注释信息。注解信息不会改变程序的编译方式和运行方式(反射才会),实际上如果不使用反射解释(可以理解为解析、提取等,找不到合适的词来描述这一动作)注解信息,注解就不会对程序有任何影响。

注解作用

注解本身是无害(对程序无影响)的,但我们会通过反射来解释注解,并影响程序行为。
注解一般用于IDE静态检查代码是否符合某些规范,有些注解可以用于生成文档,应用中用得更多的是可以动态改变程序行为的注解,如:Spring中的@Cacheable等,也有一些仅仅只是一个标记(没有任何属性)。
实际上注解种类繁多且应用广泛,而且对代码的侵入性相对较小(不解释就不生效)。

实现原理

注解本质是一个继承了Annotation的特殊接口,我们可以用反射从类、方法、字段、参数等对象中取得它们的信息,如果需要实现注解申明的功能,就需要使用反射API解释注解信息,根据注解提供的标记(注解本身)、属性等来动态改变程序行为,如:Spring的@Cacheable注解,后面我们会模拟这一过程。实际应用中不太可能在业务代码中写一堆反射代码,所以我们通常会用动态代理的方式,为目标类(接口)生成应用了注解的动态代理,来简化应用过程。

元注解

我们在编写注解时并非无章可循,需要借助一些基础的注解来实现,这些注解被称作元注解,java.lang.annotation提供了四种元注解:

JDK常用注解

JDK中提供的注解多用于标记(提供给IDE检查用),一般推荐使用。

常用框架注解

注解因为使用方法,所以在框架和库中被广为使用,典型的像Spring、Mybatis等。

Spring
Mybatis

自定义注解

除了几个元注解,JDK及开源框架中的注解也都是相应的程序员来实现的,有些注解确实很实用,那么在我们日常开发中,有些功能和特性不妨用自定义注解来实现,代码会更优雅,复用程度也会更高(个人觉得类似Cloneable、Serializable这样的标记接口,换成注解来实现会不会更优雅)。
下面以缓存注解为例演示自定义注解的定义及解释(应用)过程。

定义缓存及驱逐缓存注解

定义两个注解@Cacheable@CacheEvict分别描述缓存和删除缓存逻辑(只作演示用,完整功能请参考Spring的实现)

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cacheable {

    /**
     * 缓存前缀
     *
     * @return
     */
    String prefix() default "";

    /**
     * 缓存前缀,相当于prefix的别名,value表示是一个默认属性(当只有这一个属性时,可以省略属性名)
     *
     * @return
     */
    String value() default "";

    /**
     * 缓存版本
     *
     * @return
     */
    int version() default 0;

}

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CacheEvict {

    /**
     * 缓存前缀
     * @return
     */
    String prefix() default "";

    /**
     * 缓存前缀,相当于prefix的别名,value表示是一个默认属性(当只有这一个属性时,可以省略属性名)
     * @return
     */
    String value() default "";

    /**
     * 缓存版本
     * @return
     */
    int version() default 0;

}

我们有一个业务接口及实现

public interface UserService {

    @Cacheable(prefix = "user", version = 16)
    String get(long userId);

    @CacheEvict(prefix = "user", version = 16)
    void update(long userId, String name);

}

public class UserServiceImpl implements UserService {

    @Override
    public String get(long userId) {
        System.out.println("执行查询逻辑!");
        // 随机休眠[0, 256)毫秒模拟程序实际执行过程
        try {
            Thread.sleep(new Random().nextLong() & 255L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return String.format("user-%d", userId);
    }

    @Override
    public void update(long userId, String name) {
        System.out.println("执行更新逻辑!");
    }
}

测试一下这个业务实现

    private UserService service;

    @Before
    public void init() {
        service = new UserServiceImpl();
    }

    @Test
    public void get() throws Exception {
        long time = System.currentTimeMillis();
        String name = service.get(10086L);
        System.out.println(String.format("程序执行耗时:%d毫秒", System.currentTimeMillis() - time));
        assertEquals("user-10086", name);
    }

从测试结果来看,方法会执行实现类中的逻辑,性能相对较差(使用休眠模拟,在[0, 256)毫秒范围内)。

使用反射与动态代理解释并应用自定义注解

实际我在接口上添加了缓存注解,所以需要使用反射解释该注解,应用缓存来优化业务接口。

使用反射解释注解

    private UserService service;

    @Before
    public void init() {
        service = new UserServiceImpl();
    }

    @Test
    public void cache() throws NoSuchMethodException {
        // 假设HashMap是我们的缓存
        HashMap<String, Object> cache = new HashMap<>();

        // 假设我们调用是像下面这样的
        // String name = service.get(10086L);
        String name = null;

        // 使用反射获取方法上的注解
        Method method = service.getClass().getDeclaredMethod("get", long.class);
        Cacheable cacheable = method.getAnnotation(Cacheable.class);
        if (cacheable != null) {
            // 解释该注解里的配置项
            String prefix = cacheable.prefix();
            if (prefix.length() == 0) {
                prefix = cacheable.value();
            }
            // 当设置了缓存键
            if (prefix.length() > 0) {
                // 1. 继续取出version等信息,这里简化处理,忽略这两项
                int version = cacheable.version();
                // 2. 设置了缓存键,所以将方法执行结果缓存(如果缓存中未命中)
                String key = String.format("%s:%d:%d", prefix, 10086, version);
                if (cache.containsKey(key)) {
                    name = (String) cache.get(key);
                } else {
                    name = service.get(10086L);
                    cache.put(key, name);
                }
            }

        } else {
            name = service.get(10086L);
        }

        assertEquals("user-10086", name);

    }

代码描述的使用反射解释注解和应用注解的过程,但在实际开发中,不会在业务代码中夹杂这么多的反射代码,所以我们把它封装成动态代理工厂类,简化业务端代码。

使用动态代理封装注解解释过程

定义一个动态代理工厂类,封装动态代理生成过程,动态代理代码里解释了缓存注解,应实现了其声明的功能。

public class CacheProxyFactory {

    /**
     * 仅作测试,这里不考虑并发情况
     */
    private static final HashMap<String, Object> CACHE_STORAGE = new HashMap<>();

    public static final <T> T createProxyInstance(T target) {

        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new CacheInvocationHandler(target));
    }

    private static class CacheInvocationHandler<T> implements InvocationHandler {

        private final T target;

        public CacheInvocationHandler(T target) {
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object r = null;
            // 从target和method中提取注解信息
            Cacheable cacheable = method.getAnnotation(Cacheable.class);
            CacheEvict cacheEvict = method.getAnnotation(CacheEvict.class);
            if (cacheable != null) {
                r = cache(cacheable, method, args);
            } else if (cacheEvict != null) {
                r = remove(cacheEvict, method, args);
            } else {
                r = method.invoke(target, args);
            }

            return r;
        }

        /**
         * 处理@Cacheable注解
         *
         * @param cacheable
         * @param method
         * @param args
         * @return
         * @throws InvocationTargetException
         * @throws IllegalAccessException
         */
        private Object cache(Cacheable cacheable, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
            Object r = null;
            // 解释该注解里的配置项
            String prefix = cacheable.prefix();
            if (prefix.length() == 0) {
                prefix = cacheable.value();
            }
            // 当设置了缓存键
            if (prefix.length() > 0) {
                // 1. 继续取出version等信息,这里简化处理,忽略这两项
                int version = cacheable.version();
                // 2. 设置了缓存键,所以将方法执行结果缓存(如果缓存中未命中)
                String key = String.format("%s:%d:%d", prefix, 10086, version);
                if (CACHE_STORAGE.containsKey(key)) {
                    r = CACHE_STORAGE.get(key);
                } else {
                    r = method.invoke(target, args);
                    CACHE_STORAGE.put(key, r);
                }
            } else {
                // 应该抛出异常(使用该注解,必须配置value或prefix属性)
            }
            return r;
        }

        /**
         * 处理@CacheEvict注解
         *
         * @param cacheEvict
         * @param method
         * @param args
         * @return
         * @throws InvocationTargetException
         * @throws IllegalAccessException
         */
        private Object remove(CacheEvict cacheEvict, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
            // 解释该注解里的配置项
            String prefix = cacheEvict.prefix();
            if (prefix.length() == 0) {
                prefix = cacheEvict.value();
            }
            if (prefix.length() > 0) {
                int version = cacheEvict.version();
                CACHE_STORAGE.remove(String.format("%s:%d:%d", prefix, 10086, version));
            } else {
                // 应该抛出异常(使用该注解,必须配置value或prefix属性)
            }
            return method.invoke(target, args);
        }

    }
}

测试应用了缓存注解的代理对原业务接口性能的提升

    private UserService service;

    @Before
    public void init() {
        service = new UserServiceImpl();
    }

    @Test
    public void proxy() {

        // 生成代理
        service = CacheProxyFactory.createProxyInstance(service);

        System.out.println("-- 1 --");
        long time = System.currentTimeMillis();
        String name = service.get(10086L);
        // 程序执行耗时:112毫秒
        System.out.println(String.format("程序执行耗时:%d毫秒", System.currentTimeMillis() - time));
        assertEquals("user-10086", name);

        System.out.println("-- 2 --");
        time = System.currentTimeMillis();
        name = service.get(10086L);
        // 程序执行耗时:0毫秒
        System.out.println(String.format("程序执行耗时:%d毫秒", System.currentTimeMillis() - time));
        assertEquals("user-10086", name);

        // 执行更新方法移除缓存(请忽略实际更新逻辑)
        service.update(10086L, "Peter");

        System.out.println("-- 3 --");
        time = System.currentTimeMillis();
        name = service.get(10086L);
        // 程序执行耗时:243毫秒
        System.out.println(String.format("程序执行耗时:%d毫秒", System.currentTimeMillis() - time));
        assertEquals("user-10086", name);
    }

通过测试结果可以看出,第二次调用get方法时,直接走了缓存,所以性能有了大幅提升(实际提升效果视缓存的实现方案而定)。并且在执行update方法后,缓存被清空,再次调用get方法时,又重新初始化了缓存,从而实现了完整的@Cacheable和@CacheEvict注解功能。
这套缓存注解仅仅是从概念是模拟了Spring的缓存注解,相比之下,Spring提供了更完整的功能和程序健壮性,所以应用开发中推荐使用。

结语

自定义注解与反射和动态代理是紧密相连的,所以要掌握自定义注解,反射和动态代理技术是前置条件,不了解的请阅读笔者前面的文章

源码仓库:

上一篇下一篇

猜你喜欢

热点阅读