注解
使用注解
什么是注解(Annotation)?注解是放在Java源码的类、方法、字段、参数前的一种特殊“注释”:
// this is a component:
@Resource("hello")
public class Hello {
@Inject
int n;
@PostConstruct
public void hello(@Param String name) {
System.out.println(name);
}
@Override
public String toString() {
return "Hello";
}
}
注释会被编译器直接忽略,注解则可以被编译器打包进入class文件,因此,注解是一种用作标注的“元数据”。
注解的作用
从JVM的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。
Java的注解可以分为三类:
第一类是由编译器使用的注解,例如:
@Override
:让编译器检查该方法是否正确地实现了覆写;@SuppressWarnings
:告诉编译器忽略此处代码产生的警告。
这类注解不会被编译进入.class
文件,它们在编译后就被编译器扔掉了。
第二类是由工具处理.class
文件使用的注解,比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。
这类注解会被编译进入.class
文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。
第三类是在程序运行期能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。例如,一个配置了@PostConstruct
的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。
定义一个注解时,还可以定义配置参数。配置参数可以包括:
- 所有基本类型;
- String;
- 枚举类型;
- 基本类型、String、Class以及枚举的数组。
因为配置参数必须是常量,所以,上述限制保证了注解在定义时就已经确定了每个参数的值。
注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。
此外,大部分注解会有一个名为value
的配置参数,对此参数赋值,可以只写常量,相当于省略了value
参数。
如果只写注解,相当于全部使用默认值。
举个栗子,对以下代码:
public class Hello {
@Check(min=0, max=100, value=55)
public int n;
@Check(value=99)
public int p;
@Check(99) // @Check(value=99)
public int x;
@Check
public int y;
}
@Check
就是一个注解。第一个@Check(min=0, max=100, value=55)
明确定义了三个参数,第二个@Check(value=99)
只定义了一个value
参数,它实际上和@Check(99)
是完全一样的。最后一个@Check
表示所有参数都使用默认值。
定义注解
Java语言使用@interface
语法来定义注解(Annotation
),它的格式如下:
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
注解的参数类似无参数方法,可以用default
设定一个默认值(强烈推荐)。最常用的参数应当命名为value
。
元注解
有一些注解可以修饰其他注解,这些注解就称为元注解(meta annotation)。Java标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。
@Target
最常用的元注解是@Target
。使用@Target
可以定义Annotation
能够被应用于源码的哪些位置:
- 类或接口:
ElementType.TYPE
; - 字段:
ElementType.FIELD
; - 方法:
ElementType.METHOD
; - 构造方法:
ElementType.CONSTRUCTOR
; - 方法参数:
ElementType.PARAMETER
。
例如,定义注解@Report
可用在方法上,我们必须添加一个@Target(ElementType.METHOD)
:
@Target(ElementType.METHOD)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
定义注解@Report
可用在方法或字段上,可以把@Target
注解参数变为数组{ ElementType.METHOD, ElementType.FIELD }
:
@Target({
ElementType.METHOD,
ElementType.FIELD
})
public @interface Report {
...
}
实际上@Target
定义的value
是ElementType[]
数组,只有一个元素时,可以省略数组的写法。
@Retention
另一个重要的元注解@Retention
定义了Annotation
的生命周期:
- 仅编译期:
RetentionPolicy.SOURCE
; - 仅class文件:
RetentionPolicy.CLASS
; - 运行期:
RetentionPolicy.RUNTIME
。
如果@Retention
不存在,则该Annotation
默认为CLASS
。因为通常我们自定义的Annotation
都是RUNTIM
E,所以,务必要加上@Retention(RetentionPolicy.RUNTIME)
这个元注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Repeatable
使用@Repeatable
这个元注解可以定义Annotation
是否可重复。这个注解应用不是特别广泛。
@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Target(ElementType.TYPE)
public @interface Reports {
Report[] value();
}
经过@Repeatable
修饰后,在某个类型声明处,就可以添加多个@Report
注解:
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
@Inherited
使用@Inherited
定义子类是否可继承父类定义的Annotation
。@Inherited
仅针对@Target(ElementType.TYPE)
类型的annotation
有效,并且仅针对class
的继承,对interface
的继承无效:
@Inherited
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
在使用的时候,如果一个类用到了@Report
:
@Report(type=1)
public class Person {
}
则它的子类默认也定义了该注解:
public class Student extends Person {
}
如何定义Annotation
第一步,用@interface
定义注解:
public @interface Report {
}
第二步,添加参数、默认值:
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
把最常用的参数定义为value()
,推荐所有参数都尽量设置默认值。
第三步,用元注解配置注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
其中,必须设置@Target
和@Retention
,@Retention
一般设置为RUNTIME
,因为我们自定义的注解通常要求在运行期读取。一般情况下,不必写@Inherited
和@Repeatable
。
处理注解
Java的注解本身对代码逻辑没有任何影响。根据@Retention
的配置:
SOURCE
类型的注解在编译期就被丢掉了;CLASS
类型的注解仅保存在class文件中,它们不会被加载进JVM;RUNTIME
类型的注解会被加载进JVM,并且在运行期可以被程序读取。
如何使用注解完全由工具决定。SOURCE
类型的注解主要由编译器使用,因此我们一般只使用,不编写。CLASS
类型的注解主要由底层工具库使用,涉及到class的加载,一般我们很少用到。只有RUNTIME
类型的注解不但要使用,还经常需要编写。
因此,我们只讨论如何读取RUNTIME
类型的注解。
因为注解定义后也是一种class
,所有的注解都继承自java.lang.annotation.Annotation
,因此,读取注解,需要使用反射API。
Java提供的使用反射API读取Annotation
的方法包括:
判断某个注解是否存在于Class
、Field
、Method
或Constructor
:
- Class.isAnnotationPresent(Class)
- Field.isAnnotationPresent(Class)
- Method.isAnnotationPresent(Class)
- Constructor.isAnnotationPresent(Class)
例如:
// 判断@Report是否存在于Person类:
Person.class.isAnnotationPresent(Report.class);
使用反射API读取Annotation
:
- Class.getAnnotation(Class)
- Field.getAnnotation(Class)
- Method.getAnnotation(Class)
- Constructor.getAnnotation(Class)
例如:
// 获取Person定义的@Report注解:
Report report = Person.class.getAnnotation(Report.class);
int type = report.type();
String level = report.level();
使用反射API读取Annotation
有两种方法。方法一是先判断Annotation
是否存在,如果存在,就直接读取:
Class cls = Person.class;
if (cls.isAnnotationPresent(Report.class)) {
Report report = cls.getAnnotation(Report.class);
...
}
第二种方法是直接读取Annotation
,如果Annotation
不存在,将返回null
:
Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {
...
}
读取方法、字段和构造方法的Annotation
和Class
类似。但要读取方法参数的Annotation
就比较麻烦一点,因为方法参数本身可以看成一个数组,而每个参数又可以定义多个注解,所以,一次获取方法参数的所有注解就必须用一个二维数组来表示。
例如,对于以下方法定义的注解:
public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}
要读取方法参数的注解,我们先用反射获取Method
实例,然后读取方法参数的所有注解:
// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
if (anno instanceof Range r) { // @Range注解
r.max();
}
if (anno instanceof NotNull n) { // @NotNull注解
//
}
}
使用注解
注解如何使用,完全由程序自己决定。例如,JUnit是一个测试框架,它会自动运行所有标记为@Test
的方法。
我们来看一个@Range
注解,我们希望用它来定义一个String
字段的规则:字段长度满足@Range
的参数定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
int min() default 0;
int max() default 255;
}
在某个JavaBean中,我们可以使用该注解:
public class Person {
@Range(min=1, max=20)
public String name;
@Range(max=10)
public String city;
}
但是,定义了注解,本身对程序逻辑没有任何影响。我们必须自己编写代码来使用注解。这里,我们编写一个Person
实例的检查方法,它可以检查Person
实例的String
字段长度是否满足@Range
的定义:
void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
// 遍历所有Field:
for (Field field : person.getClass().getFields()) {
// 获取Field定义的@Range:
Range range = field.getAnnotation(Range.class);
// 如果@Range存在:
if (range != null) {
// 获取Field的值:
Object value = field.get(person);
// 如果值是String:
if (value instanceof String s) {
// 判断值是否满足@Range的min/max:
if (s.length() < range.min() || s.length() > range.max()) {
throw new IllegalArgumentException("Invalid field: " + field.getName());
}
}
}
}
}
这样一来,我们通过@Range
注解,配合check()
方法,就可以完成Person
实例的检查。注意检查逻辑完全是我们自己编写的,JVM不会自动给注解添加任何额外的逻辑。
Java
Spring
核心注解
注解 | 说明 | 支持版本 |
---|---|---|
@Component | 标识一个类为 Spring 组件 | Spring 2.5+ |
@Repository | 标识数据访问组件 | Spring 2.0+ |
@Service | 标识服务层组件 | Spring 2.0+ |
@Controller | 标识控制器组件 | Spring 2.5+ |
@RestController | @Controller + @ResponseBody | Spring 4.0+ |
@Configuration | 标识配置类 | Spring 3.0+ |
@Bean | 声明一个方法的返回值为 Bean | Spring 3.0+ |
@Autowired | 自动装配依赖 | Spring 2.5+ |
@Qualifier | 指定注入的 Bean 名称 | Spring 2.5+ |
@Primary | 优先注入的 Bean | Spring 3.0+ |
@Value | 注入属性值 | Spring 3.0+ |
@PropertySource | 加载属性文件 | Spring 3.1+ |
@Profile | 指定组件在特定环境下激活 | Spring 3.1+ |
@Scope | 定义 Bean 的作用域 | Spring 2.5+ |
@Lazy | 延迟初始化 Bean | Spring 3.0+ |
@DependsOn | 定义 Bean 初始化依赖 | Spring 3.0+ |
Web MVC 注解
注解 | 说明 | 支持版本 |
---|---|---|
@RequestMapping | 映射请求路径 | Spring 2.5+ |
@GetMapping | GET 请求映射 | Spring 4.3+ |
@PostMapping | POST 请求映射 | Spring 4.3+ |
@PutMapping | PUT 请求映射 | Spring 4.3+ |
@DeleteMapping | DELETE 请求映射 | Spring 4.3+ |
@PatchMapping | PATCH 请求映射 | Spring 4.3+ |
@RequestParam | 获取请求参数 | Spring 2.5+ |
@PathVariable | 获取路径变量 | Spring 3.0+ |
@RequestBody | 获取请求体内容 | Spring 3.0+ |
@ResponseBody | 返回内容直接写入响应体 | Spring 3.0+ |
@ModelAttribute | 绑定参数到模型对象 | Spring 2.5+ |
@SessionAttribute | 访问会话属性 | Spring 3.1+ |
@CookieValue | 获取 Cookie 值 | Spring 3.0+ |
@RequestHeader | 获取请求头值 | Spring 3.0+ |
@CrossOrigin | 跨域支持 | Spring 4.2+ |
@ExceptionHandler | 异常处理方法 | Spring 3.0+ |
@ControllerAdvice | 全局控制器增强 | Spring 3.2+ |
@ResponseStatus | 定义响应状态码 | Spring 3.0+ |
事务注解
注解 | 说明 | 支持版本 |
---|---|---|
@Transactional | 声明事务 | Spring 2.0+ |
@EnableTransactionManagement | 启用事务管理 | Spring 3.1+ |
数据访问注解
注解 | 说明 | 支持版本 |
---|---|---|
@Entity | JPA 实体类 | JPA 1.0+ |
@Table | 指定实体对应的表 | JPA 1.0+ |
@Id | 标识主键 | JPA 1.0+ |
@GeneratedValue | 主键生成策略 | JPA 1.0+ |
@Column | 指定字段映射 | JPA 1.0+ |
@OneToOne | 一对一关系 | JPA 1.0+ |
@OneToMany | 一对多关系 | JPA 1.0+ |
@ManyToOne | 多对一关系 | JPA 1.0+ |
@ManyToMany | 多对多关系 | JPA 1.0+ |
@JoinColumn | 指定关联列 | JPA 1.0+ |
@Query | 自定义查询语句 | Spring Data 1.0+ |
@Modifying | 标识修改操作 | Spring Data 1.0+ |
@EnableJpaRepositories | 启用 JPA 仓库 | Spring Data 1.0+ |
Spring Boot 特有注解
注解 | 说明 | 支持版本 |
---|---|---|
@SpringBootApplication | 主启动类注解 | Spring Boot 1.2+ |
@EnableAutoConfiguration | 启用自动配置 | Spring Boot 1.0+ |
@ComponentScan | 组件扫描 | Spring 3.1+ |
@ConditionalOnClass | 类路径存在时生效 | Spring Boot 1.0+ |
@ConditionalOnMissingBean | Bean 不存在时生效 | Spring Boot 1.0+ |
@ConditionalOnProperty | 属性条件判断 | Spring Boot 1.0+ |
@ConfigurationProperties | 绑定配置属性 | Spring Boot 1.0+ |
@EnableConfigurationProperties | 启用配置属性绑定 | Spring Boot 1.0+ |
@EnableAsync | 启用异步方法 | Spring 3.0+ |
@Async | 标识异步方法 | Spring 3.0+ |
@EnableScheduling | 启用定时任务 | Spring 3.0+ |
@Scheduled | 定时任务方法 | Spring 3.0+ |
@EnableCaching | 启用缓存 | Spring 3.1+ |
@Cacheable | 缓存方法结果 | Spring 3.1+ |
@CacheEvict | 清除缓存 | Spring 3.1+ |
@CachePut | 更新缓存 | Spring 3.1+ |
测试注解
注解 | 说明 | 支持版本 |
---|---|---|
@SpringBootTest | Spring Boot 测试 | Spring Boot 1.4+ |
@DataJpaTest | JPA 测试 | Spring Boot 1.4+ |
@WebMvcTest | MVC 控制器测试 | Spring Boot 1.4+ |
@MockBean | 模拟 Bean | Spring Boot 1.4+ |
@Test | JUnit 测试方法 | JUnit 4+ |
@BeforeEach | 每个测试方法前执行 | JUnit 5+ |
@AfterEach | 每个测试方法后执行 | JUnit 5+ |
@BeforeAll | 所有测试方法前执行 | JUnit 5+ |
@AfterAll | 所有测试方法后执行 | JUnit 5+ |
安全注解 (Spring Security)
注解 | 说明 | 支持版本 |
---|---|---|
@EnableWebSecurity | 启用 Web 安全 | Spring Security 3.2+ |
@PreAuthorize | 方法执行前权限检查 | Spring Security 3.0+ |
@PostAuthorize | 方法执行后权限检查 | Spring Security 3.0+ |
@Secured | 方法权限检查 | Spring Security 2.0+ |
@RolesAllowed | JSR-250 角色检查 | Spring Security 2.0+ |
@EnableGlobalMethodSecurity | 启用方法级安全 | Spring Security 3.0+ |
Spring Cache
@Cacheable
添加在方法上,缓存方法的执行结果。执行过程如下:
- 1)首先,判断方法执行结果的缓存。如果有,则直接返回该缓存结果。
- 2)然后,执行方法,获得方法结果。
- 3)之后,根据是否满足缓存的条件。如果满足,则缓存方法结果到缓存。
- 4)最后,返回方法结果。
@CachePut
添加在方法上,缓存方法的执行结果。不同于 @Cacheable
注解,它的执行过程如下:
- 1)首先,执行方法,获得方法结果。也就是说,无论是否有缓存,都会执行方法。
- 2)然后,根据是否满足缓存的条件。如果满足,则缓存方法结果到缓存。
- 3)最后,返回方法结果。
@CacheEvict
添加在方法上,删除缓存
Lombok
注解 | 说明 | 支持版本 |
---|---|---|
@val | 用在局部变量前面,相当于将变量声明为final | - |
@NonNull | 给方法参数增加这个注解会自动在方法内对该参数进行是否为空的校验,如果为空,则抛出NPE(NullPointerException) | - |
@Cleanup | 自动管理资源,用在局部变量之前,在当前变量范围内即将执行完毕退出之前会自动清理资源,自动生成try-finally这样的代码来关闭流 | - |
@Getter/@Setter | 用在属性上,再也不用自己手写setter和getter方法了,还可以指定访问范围 | - |
@ToString | 用在类上,可以自动覆写toString方法,当然还可以加其他参数,例如@ToString(exclude=”id”)排除id属性,或者@ToString(callSuper=true, includeFieldNames=true)调用父类的toString方法,包含所有属性 | - |
@EqualsAndHashCode | 用在类上,自动生成equals方法和hashCode方法 | - |
@NoArgsConstructor, @RequiredArgsConstructor and @AllArgsConstructor | 用在类上,自动生成无参构造和使用所有参数的构造函数以及把所有@NonNull属性作为参数的构造函数,如果指定staticName = “of”参数,同时还会生成一个返回类对象的静态工厂方法,比使用构造函数方便很多 | - |
@Data | 注解在类上,相当于同时使用了@ToString、@EqualsAndHashCode、@Getter、@Setter和@RequiredArgsConstrutor这些注解,对于POJO类十分有用 | - |
@Value | 用在类上,是@Data的不可变形式,相当于为属性添加final声明,只提供getter方法,而不提供setter方法 | - |
@Builder | 用在类、构造器、方法上,为你提供复杂的builder APIs,让你可以像如下方式一样调用Person.builder().name(“Adam Savage”).city(“San Francisco”).job(“Mythbusters”).job(“Unchained Reaction”).build();更多说明参考Builder | - |
@SneakyThrows | 自动抛受检异常,而无需显式在方法上使用throws语句 | - |
@Synchronized | 用在方法上,将方法声明为同步的,并自动加锁,而锁对象是一个私有的属性$lock或$LOCK,而java中的synchronized关键字锁对象是this,锁在this或者自己的类对象上存在副作用,就是你不能阻止非受控代码去锁this或者类对象,这可能会导致竞争条件或者其它线程错误 | - |
@Getter(lazy=true) | 可以替代经典的Double Check Lock样板代码 | - |
@Log | 根据不同的注解生成不同类型的log对象,但是实例名称都是log,有六种可选实现类 | - |
@CommonsLog
Creates log = org.apache.commons.logging.LogFactory.getLog(LogExample.class);@Log
Creates log = java.util.logging.Logger.getLogger(LogExample.class.getName());@Log4j
Creates log = org.apache.log4j.Logger.getLogger(LogExample.class);@Log4j2
Creates log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class);@Slf4j
Creates log = org.slf4j.LoggerFactory.getLogger(LogExample.class);@XSlf4j
Creates log = org.slf4j.ext.XLoggerFactory.getXLogger(LogExample.class);
Hibernate Validator
注解 | 说明 | 支持版本 |
---|---|---|
@NotBlank | 只能用于字符串不为 null ,并且字符串 #trim() 以后 length 要大于 0 | - |
@NotEmpty | 集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为 null | - |
@NotNull | 不能为 null | |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 | - |
@Max(value) | 该字段的值只能小于或等于该值 | - |
@Min(value) | 该字段的值只能大于或等于该值 | - |
@Range(min=, max=) | 检被注释的元素必须在合适的范围内 | - |
@Size(max, min) | 检查该字段的 size 是否在 min 和 max 之间,可以是字符串、数组、集合、Map 等 | - |
@Length(max, min) | 被注释的字符串的大小必须在指定的范围内。 | - |
@AssertFalse | 被注释的元素必须为 false | - |
@AssertTrue | 被注释的元素必须为 true | - |
被注释的元素必须是电子邮箱地址 | - | |
@URL(protocol=,host=,port=,regexp=,flags=) | 被注释的字符串必须是一个有效的 URL | - |
@Null | 必须为 null | - |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 | - |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | - |
@Digits(integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 | - |
@Positive | 判断正数 | - |
@PositiveOrZero | 判断正数或 0 | - |
@Negative | 判断负数 | - |
@NegativeOrZero | 判断负数或 0 | - |
@Future | 被注释的元素必须是一个将来的日期 | - |
@FutureOrPresent | 判断日期是否是将来或现在日期 | - |
@Past | 检查该字段的日期是在过去 | - |
@PastOrPresent | 判断日期是否是过去或现在日期 | - |
@SafeHtml | 判断提交的 HTML 是否安全。例如说,不能包含 JavaScript 脚本等等 | - |
easyExcel
注解 | 说明 | 支持版本 |
---|---|---|
@ExcelProperty | 这是最常用的一个注解,注解中有三个参数value,index,converter分别代表列明,列序号,数据转换方式,value和index只能二选一,通常不用设置converter | - |
@ColumnWith | 用于设置列宽度的注解,注解中只有一个参数value,value的单位是字符长度,最大可以设置255个字符,因为一个excel单元格最大可以写入的字符个数就是255个字符。 | - |
@ContentFontStyle | 用于设置单元格内容字体格式的注解 | - |
@ContentLoopMerge | 用于设置合并单元格的注解 | - |
@ContentRowHeight | 用于设置行高 | - |
@ContentStyle | 设置内容格式注解 | - |
@HeadFontStyle | 用于定制标题字体格式 | - |
@HeadRowHeight | 设置标题行行高 | - |
@HeadStyle | 设置标题样式 | - |
@ExcelIgnore | 不将该字段转换成Excel | - |
@ExcelIgnoreUnannotated | 没有注解的字段都不转换 | - |
References
https://liaoxuefeng.com/books/java/annotation/index.html
https://learnku.com/docs/springboot-like-laravel/spring-zhu-jie-da-quan/16925
https://sites.google.com/site/javahuide9/brief/lombok-in-10-minutes