Java拾遗:015 - Java注解与自定义注解
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提供了更完整的功能和程序健壮性,所以应用开发中推荐使用。
结语
自定义注解与反射和动态代理是紧密相连的,所以要掌握自定义注解,反射和动态代理技术是前置条件,不了解的请阅读笔者前面的文章
源码仓库: