Java注解

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

注解作用

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

实现原理

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

元注解

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

  • @Documented 这是一个标记注解(没有任何属性),用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被像javadoc这样的工具文档化
  • @Inherited 这也是一个标记注解,被其标注的类注解是可以被继承的,例如:在父类中使用了被@Inherited注解标记的注解,那么其子类将自动继承该注解
  • @Retention 描述注解的生命周期,其值由RetentionPolicy枚举类决定,包含:SOURCE(表示只在源码阶段有效,编译阶段丢弃)、CLASS(在类加载的时候丢弃)、RUNTIME(始终保留,运行期也存在,所以我们可以在运行期使用反射来解释这类注解,一般自定义注解时会使用这种方式)
  • @Target 表示注解应用范围,其值由ElementType枚举类决定,包含:TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE、ANNOTATION_TYPE、PACKAGE、TYPE_PARAMETER、TYPE_USE(见名知意,这里就不再解释了)。
JDK常用注解
  • @Override 用于IDE检查子类是否正确重写了父类方法,建议在编写子类时使用,避免手误等问题。
  • @Deprecated 用于标记类、接口、方法等不推荐使用,表示后续不再支持(可能会移除),一般遇到这种注解,那么应尽量避免使用被其标记的类、接口、方法,避免后续升级版本过程中造成代码不兼容。
  • @SuppressWarnings 用于去除一些警告,比如:@SuppressWarnings("unchecked")

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

常用框架注解

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

Spring
  • @Component
  • @Controller
  • @Service
  • @Repository
  • @Autowired
  • @RequestMapping
  • ... ... Spring生态体系(Spring Framework、Spring MVC、Spring Boot、Spring Cloud等)中的框架大量使用的注解,这里不再一一列举
Mybatis
  • @Insert
  • @Select
  • @Update
  • @Delete
  • @Param
  • @Results
  • @Result
  • ... ... Mybatis里同样使用了大量的注解,但个人不太推荐使用类似@Insert这样的注解(该注解用于编写插入SQL语句)来实现业务逻辑,SQL与Java代码耦合在一起,这跟不使用注解直接把SQL写在Java代码中也没什么分别了,相比之下写在XML方便统一管理会更为合适(以上为个人愚见,不喜勿喷)。

自定义注解

除了几个元注解,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提供了更完整的功能和程序健壮性,所以应用开发中推荐使用。