前言:Java 注解,对于很多人都不陌生了,但是在公司的实际开发中,可能让我们自己去定义注解并应用到生产环境中的机会比较少,所以会导致一部分人对注解的理解比较浅,在看到一些框架或者别人的代码中有注解的代码就会头大,不知道这代表的是什么意思。其实究其原因,最主要的还是我们对注解的这个概念没搞清楚,所以本文通篇会串插着这些重要的概念,从几个主题入手,从浅到深的带领大家进入注解的世界,让大家在今后的代码旅程中轻松应对注解。
注解是 Java 5 的一个新特性。先给大家梳理一个在生产环境中注解的常用使用流程。第一步: 定义一个注解;第二步: 在类上面或方法上面等地方使用注解;第三步: 通过注解处理器获取注解信息并做相应的处理(这个处理也就是注解真正起作用的地方了)。本篇文章的整个脉络大致也是按照这三步来写的。
JDK 内置注解
先来看几个 Java 内置的注解,让大家热热身。
- @Override 演示
class Parent {
public void run() {
}
}
class Son extends Parent {
/**
* 这个注解是为了检查此方法是否真的是重写父类的方法
* 这时候就不用我们用肉眼去观察到底是不是重写了
*/
@Override
public void run() {
}
}
- @Deprecated 演示
class Parent {
/**
* 此注解代表过时了,但是如果可以调用到,当然也可以正常使用
* 但是,此方法有可能在以后的版本升级中会被慢慢的淘汰
* 可以放在类,变量,方法上面都起作用
*/
@Deprecated
public void run() {
}
}
public class JDKAnnotationDemo {
public static void main(String[] args) {
Parent parent = new Parent();
parent.run(); // 在编译器中此方法会显示过时标志
}
}
- @SuppressWarnings 演示
class Parent {
// 因为定义的 name 没有使用,那么编译器就会有警告,这时候使用此注解可以屏蔽掉警告
// 即任意不想看到的编译时期的警告都可以用此注解屏蔽掉,但是不推荐,有警告的代码最好还是处理一下
@SuppressWarnings("all")
private String name;
}
- @FunctionalInterface 演示
/**
* 此注解是 Java8 提出的函数式接口,接口中只允许有一个抽象方法
* 加上这个注解之后,类中多一个抽象方法或者少一个抽象方法都会报错
*/
@FunctionalInterface
interface Func {
void run();
}
定义一个注解
完整注解
这里先提供一个比较完整的注解,即此注解内容基本包含了我们在生产环境中常用的信息了,然后根据这个注解来介绍枚举的重要概念。首先看到下面这个完整注解包含三部分内容,第一部分就是 @interface 来定义注解,所有注解隐式继承 java.lang.annotation.Annotation;第二部分就是这个注解的内部信息;第三部分就是这个注解的头部,即元注解。
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
@Inherited // 此注解应用的相对较少,本文不多做介绍,可自行研究
@Documented // 此注解应用的相对较少,本文不多做介绍,可自行研究
public @interface ApiAuthAnnotation {
int age();
String name() default "lyf";
AnnotationDemo annotattion() default @AnnotationDemo;
Season Sason() default Season.SPRING;
int[] nums() default {1, 2, 3};
Class clazz() default String.class;
}
注解内部信息
所谓注解内部信息,也就是这个注解里面都写什么,通过如上的完整注解可以很直观的看到都写了什么,其实就是一种比较特殊的方法了,带有返回值类型,没有具体实现,后面可以选填默认值。大概也就这么点内容了。其支持的数据类型有:
- 基本数据类型
- java.lang.String
- 注解
- 枚举
- 一维数组
- java.lang.Class
元注解
元注解是指用于注解类型上的注解。通俗来讲,元注解的作用就是,规范我们定义的注解,给这个注解一些规则。比如说: 我在注解头部没有写元注解,那么我们自定义的这个注解在使用的时候放在类上面,方法上面,或者放在成员变量上面,没有规则,想放哪里就放哪里;但是当我们在注解头部写了元注解,并把元注解的 Target 设置为 ElementType.METHOD,那么我们自定义的这个注解在使用的时候就只能放在方法上面,放在类上面或者成员变量上面就会报错。
元注解的注解类型主要有如下几种:
-
@Target: 表示定义的注解的作用域,其值包括:
- CONSTRUCTOR: 构造方法声明;
- FIELD: 属性/字段声明;
- LOCAL_VARIABLE: 局部变量声明;
- METHOD: 方法声明;
- PACKAGE: 包声明;
- PARAMETER: 参数声明;
- TYPE: 类接口声明;
- ANNOTATION_TYPE: 注解类型声明;
- TYPE_PARAMETER: 类型参数声明(jdk1.8 提供);
- TYPE_USE: 类型使用(jdk1.8 提供)
-
@Retention: 自定义注解的生命周期,其值包括:
- SOURCE: 只在源码显示,编译时丢弃;(比如 jdk 自带的 @SuppressWarnings 和 @Override)
- CLASS: 编译时记录到.class中,运行时忽略;
- RUNTIME: 运行时存在,可通过反射来读取。(很重要,生产中我们开发常用此值)
- @Inherited: 表示注解是否可被子元素继承。
- @Documented: 表示 javadoc 是否为实例产生文档
注意事项
- 注解不能继承另一个注解
- 方法不能有参数
- 方法不能抛出异常
- 不能定义 Object 或 Annotation 接口中的方法(因为注解都隐式继承 Annotation 接口)
- 注解不能是泛型
- 使用 default 指定属性的默认值
- 不能使用 null 作为属性的默认值
使用注解
这个当然就相当简单了,哪个地方需要使用我们自定义的注解,那我们在其地方添加上注解就 ok 了。如下代码: 如果要作用到类,那就在类头上加上注解;如果要作用到方法,那就在方法上加上注解。
@ApiAuthAnnotation(age = 16, name = "lyf")
@RestController
public class ApiController {
@ApiAuthAnnotation(age = 20, name = "lisi")
@RequestMapping(value = "/topic1", method = RequestMethod.GET)
public void getByTopic1() {
System.out.println("年龄大于 18 岁的可以访问此 API");
}
@ApiAuthAnnotation(age = 16, name = "zhangsan")
@RequestMapping(value = "/topic2", method = RequestMethod.GET)
public void getByTopic2() {
System.out.println("年龄小于 18 岁的不可以访问此 API");
}
}
从如上代码来看,使用注解的时候则是相当的简单了,我们这里是在类上和方法上都加上了自定义的注解了,至于这些注解加上以后怎么起作用,那就跟注解处理器有关了。
【注: 在使用注解的时候其实还有很多小的语法技巧,很多时候可以简写一些代码,不过有没有必要简写代码,这个自己去衡量,这个不是重点,就是语法,可以自行去研究下。就比如当注解只要一个元素,且其名字是 value 时,赋值的时候可以简写】
注解处理器
注解处理器才是使用注解整个流程中最重要的一步了。所有在代码中出现的注解,它到底起了什么作用,都是在注解处理器中定义好的。
概念:注解本身并不会对程序的编译方式产生影响,而是注解处理器起的作用;注解处理器能够通过在运行时使用反射获取在程序代码中的使用的注解信息,从而实现一些额外功能。前提是我们自定义的注解使用的是 RetentionPolicy.RUNTIME 修饰的。这也是我们在开发中使用频率很高的一种方式。
我们先来了解下如何通过在运行时使用反射获取在程序中的使用的注解信息。如下类注解和方法注解。
类注解
Class aClass = ApiController.class;
Annotation[] annotations = aClass.getAnnotations();
for(Annotation annotation : annotations) {
if(annotation instanceof ApiAuthAnnotation) {
ApiAuthAnnotation apiAuthAnnotation = (ApiAuthAnnotation) annotation;
System.out.println("name: " + apiAuthAnnotation.name());
System.out.println("age: " + apiAuthAnnotation.age());
}
}
方法注解
Method method = ... //通过反射获取方法对象
Annotation[] annotations = method.getDeclaredAnnotations();
for(Annotation annotation : annotations) {
if(annotation instanceof ApiAuthAnnotation) {
ApiAuthAnnotation apiAuthAnnotation = (ApiAuthAnnotation) annotation;
System.out.println("name: " + apiAuthAnnotation.name());
System.out.println("age: " + apiAuthAnnotation.age());
}
}
此部分内容可参考: 通过反射获取注解信息
注解处理器实战
接下来我通过在公司中的一个实战改编来演示一下注解处理器的真实使用场景。
需求: 网站后台接口只能是年龄大于 18 岁的才能访问,否则不能访问
前置准备: 定义注解(这里使用上文的完整注解),使用注解(这里使用上文中使用注解的例子)
接下来要做的事情: 写一个切面,拦截浏览器访问带注解的接口,取出注解信息,判断年龄来确定是否可以继续访问。
在 dispatcher-servlet.xml 文件中定义 aop 切面
<aop:config>
<!--定义切点,切的是我们自定义的注解-->
<aop:pointcut id="apiAuthAnnotation" expression="@annotation(cn.caijiajia.devops.aspect.ApiAuthAnnotation)"/>
<!--定义切面,切点是 apiAuthAnnotation,切面类即注解处理器是 apiAuthAspect,主处理逻辑在方法名为 auth 的方法中-->
<aop:aspect ref="apiAuthAspect">
<aop:around method="auth" pointcut-ref="apiAuthAnnotation"/>
</aop:aspect>
</aop:config>
切面类处理逻辑即注解处理器代码如
@Component("apiAuthAspect")
public class ApiAuthAspect {
public Object auth(ProceedingJoinPoint pjp) throws Throwable {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
ApiAuthAnnotation apiAuthAnnotation = method.getAnnotation(ApiAuthAnnotation.class);
Integer age = apiAuthAnnotation.age();
if (age > 18) {
return pjp.proceed();
} else {
throw new RuntimeException("你未满18岁,禁止访问");
}
}
}
总结
注解其实在我们的代码中用的也越来越多了,特别是框架里面都会提供很多的注解,把注解的概念搞清楚之后,对于以后看别人的代码和框架源码都很多帮助,所以平时没事也可以多了解了解。比如我们经常使用的 @Autowired 注解,其背后做了什么事情,有兴趣都可以去了解一下。再比如,权限框架 Shiro,里面也提供很多注解来管理权限,他们是怎么使用注解和处理注解的,都可以去研究下。有什么问题,欢迎老铁们一起交流。。。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。