大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 012 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。
–
注解(Annotations)是 Java 编程语言中的一个强大特性,能够为代码添加元数据,提供额外的信息和行为。注解在 Java SE 5 中引入,从此以后,它们被广泛用于各种场景,包括代码生成、运行时处理和编译时检查等。
文章目录
1、Java 注解简介
Java 注解(Annotations)是自 JDK 1.5 引入的一种特性,它们提供了一种在代码中嵌入额外信息的机制,这些信息不会直接影响代码的执行,但可用于编译检查、代码分析、或在运行时的处理。注解可以被用来注释包、类、接口、字段、方法参数、局部变量等。
一般常用的注解可以分为三类:
- 一类是 Java 自带的标准注解,包括
@Override
(标明重写某个方法)、@Deprecated
(标明某个类或方法过时)和@SuppressWarnings
(标明要忽略的警告),使用这些注解后编译器就会进行检查; - 一类为元注解,元注解是用于定义注解的注解,包括
@Retention
(标明注解被保留的阶段)、@Target
(标明注解使用的范围)、@Inherited
(标明注解可继承)、@Documented
(标明是否生成 javadoc 文档); - 一类为自定义注解,可以根据自己的需求定义注解。
2、内置注解类型
Java 提供了一系列内置注解,这些注解在 Java 的标准库中定义,并被广泛用于各种编程场景中。了解这些内置注解是掌握 Java 注解功能的基础。以下是几个常用的 Java 内置注解的详细说明:
2.1、 @Override
用途: 指明一个方法声明打算重写超类中的另一个方法声明。
功能: 如果被注解的方法并没有正确地重写基类中的方法(例如,方法签名不正确),编译器将生成错误,这有助于避免错误和提升代码的可读性。
示例:
class ParentClass {
public void display() {
System.out.println("Parent display()");
}
}
class ChildClass extends ParentClass {
@Override
public void display() {
System.out.println("Child display()");
}
}
2.2、 @Deprecated
用途: 标记不再推荐使用的类、方法或字段,表明它们可能在将来的版本中被移除。
功能: 使用被@Deprecated
标注的元素时,编译器会生成警告,提示开发者寻找替代方案。
示例:
class Example {
@Deprecated
public void oldMethod() {
System.out.println("Old way of doing this");
}
}
2.3、 @SuppressWarnings
用途: 指示编译器忽略特定的警告信息。
功能: 这有助于减少不必要的编译器警告,特别是当你确定某些警告对于当前代码是无害的时候。
参数: 接受一个字符串数组,每个字符串指示一种不应被报告的警告。
示例:
// 忽略未经检查的转换警告
@SuppressWarnings("unchecked")
public void myMethod() {
List rawList = new ArrayList();
List<String> list = rawList;
}
3、元注解
Java 中的元注解是用来定义其他注解的注解。这些元注解提供了一种方式来指定注解的行为和应用范围。下面是一些常用的元注解以及它们的用途和应用方式:
3.1、@Target
用途: 指定注解可以应用的 Java 元素类型(如类、字段、方法、参数等)。
参数:
ElementType.TYPE
- 应用于类、接口或枚举声明;ElementType.FIELD
- 应用于字段或属性;ElementType.METHOD
- 应用于方法级注解;ElementType.PARAMETER
- 应用于方法的参数;ElementType.CONSTRUCTOR
- 应用于构造函数;ElementType.LOCAL_VARIABLE
- 应用于局部变量;ElementType.ANNOTATION_TYPE
- 应用于注解;ElementType.PACKAGE
- 应用于包声明。
示例:
// 只能被用于方法
@Target(ElementType.METHOD)
public @interface MyMethodAnnotation { }
3.2、@Retention
用途: 指定注解在哪个级别上可用,即注解的信息保留到哪个阶段。
参数:
RetentionPolicy.SOURCE
- 注解只在源码中可用,编译时被丢弃RetentionPolicy.CLASS
- 注解在编译时被记录在类文件中,但运行时不需要 JVM 保留RetentionPolicy.RUNTIME
- 注解在运行时保留,可通过反射读取
示例:
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRuntimeAnnotation { }
3.3、@Documented
用途: 指示将此注解包括在 Javadoc 中。
功能: 当开发者使用此注解时,注解的信息会出现在 Javadoc 生成的文档中,提高了代码的文档化水平。
示例:
@Documented
public @interface MyDocumentedAnnotation { }
3.4、@Inherited
用途: 允许子类继承父类中的注解。
功能: 默认情况下,注解不会从超类继承到子类,但如果在注解声明时使用了@Inherited
,则子类可以继承父类的该注解。
示例:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInheritedAnnotation { }
通过使用这些元注解,Java 开发者可以精确控制自定义注解的行为和应用方式,从而在不同的编程场景中实现更加灵活和强大的代码管理。
4、自定义注解
在 Java 中定义自定义注解是通过使用@interface
关键字完成的,这个关键字告诉Java编译器你正在定义一个注解。定义注解时,你可以指定一些元注解来控制注解的行为,比如@Retention
和@Target
。此外,注解可以包含元素,这些元素可以看作是注解的方法,用于提供值。
4.1、如何定义一个注解
定义一个注解的基本结构如下:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 定义注解的保留策略
@Retention(RetentionPolicy.RUNTIME)
// 定义注解可以应用的目标
@Target(ElementType.METHOD)
public @interface MyCustomAnnotation {
// 注解的一个元素
String description();
// 带有默认值的注解元素
int value() default 1;
}
在这个例子中,MyCustomAnnotation
注解被定义为可以用在方法上,并且在运行时仍然保留。它有两个元素:description
和 value
,其中 value
元素有一个默认值 1
。
4.2、注解的属性(元素)及默认值
注解的元素定义类似于接口中的方法定义,但它们可以有默认值。如果在使用注解时未指定元素的值,则使用默认值。如果元素没有默认值,则使用注解时必须为该元素提供值。
4.3、举例说明自定义注解的使用场景
4.3.1、数据验证
自定义注解可以用于简化数据验证逻辑。例如,你可以创建一个注解来验证方法参数是否符合特定条件:
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotEmpty {
}
public class Validator {
public void process(@NotEmpty String str) {
if (str == null || str.isEmpty()) {
throw new IllegalArgumentException("The string cannot be empty");
}
}
}
4.3.2、日志记录
自定义注解可以用于标记那些需要自动记录日志的方法:
java
复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {
LogLevel level() default LogLevel.INFO;
}
public class LoggingAspect {
@Loggable(level = LogLevel.DEBUG)
public void myMethod() {
// 方法实现
}
}
enum LogLevel {
DEBUG, INFO, WARN, ERROR
}
在这个场景中,LoggingAspect
类中的 myMethod
方法被标记为日志记录方法,且日志级别设置为 DEBUG。
4.4.3、安全检查
自定义注解也可以用于安全性检查,例如控制方法的访问权限:
java
复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiresAuthorization {
String role();
}
public class SecurityAspect {
@RequiresAuthorization(role = "ADMIN")
public void adminOnlyOperation() {
// 只有管理员可以执行的操作
}
}
在这个示例中,adminOnlyOperation
方法被标记为只有管理员角色用户才能执行的操作。
5、注解的处理
Java 中的注解分为编译时注解和运行时注解两大类,它们的主要区别在于它们被处理的时机和方式。
- 运行时注解:通过反射在运行时动态处理注解的逻辑;
- 编译时注解:通过注解处理器在编译期动态处理相关逻辑。
5.1、运行时注解
运行时注解主要通过 Java 的反射 API 实现。反射 API 允许程序在运行时查询关于类、接口、字段和方法的信息,也可以用来调用方法和访问字段。这些注解不仅包含在 Java 源代码中,也被保留在 Java 字节码文件中,最终由 JVM 在运行时加载并可通过反射来访问。
实现原理:
- 反射: 在程序运行时,通过类对象可以访问到各种运行时注解信息,然后根据这些信息执行特定操作,如动态方法调用、修改访问权限等。
- 动态代理: 某些情况下,如在 Spring 框架中,可能使用动态代理技术来创建对象的代理实例。代理可以拦截原始对象的方法调用,执行额外的操作(如安全检查、事务处理等),然后再调用原始方法。
5.2、编译时注解
编译时注解通常在 Java 代码编译成字节码的过程中由注解处理器处理。注解处理器是一种工具,它扫描源代码中的注解并进行处理,如生成额外的源代码、编译时检查等。
实现原理:
- 注解处理器: 这是一个实现了
javax.annotation.processing.Processor
接口的类,它在编译阶段被调用。编译器会查找这些处理器并传递相关的源代码,注解处理器根据注解生成新的源代码或其他文件。 - 生成代码: 处理器可以生成 Java 代码、配置文件或其他资源,这些生成的代码会在编译时一起编译成字节码。例如,Lombok 使用注解处理器自动生成 getter 和 setter 方法。
这两种方式各自针对其适用的场景提供了有效的技术手段。,运行时注解侧重于运行时的灵活性和动态性,而编译时注解侧重于编译效率和源代码的生成或处理。
6、运行时注解实现-方法字段判空
我们这里使用 Java 的动态代理机制来实现这种功能。这通常适用于创建一个代理类,它包装原始对象并在执行方法调用之前添加额外的检查。
以下是一个示例,展示了如何使用 Java 的动态代理来实现方法字段判空这一功能:
6.1、定义注解
首先,定义注解
package com.lizhengi.annotate.notEmpty;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author lizhengi
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
String message() default "Parameter cannot be null";
}
6.2、创建代理类
使用java.lang.reflect.Proxy
来创建一个动态代理,该代理会拦截所有方法调用,并检查被@NotNull
注解标记的参数是否为null。
package com.lizhengi.annotate.notEmpty;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
/**
* 使用动态代理检查方法参数是否非空。
* 如果参数被@NotEmpty注解标记且传递为null,将抛出IllegalArgumentException。
*
* @author liziheng
*/
public class NotNullProxy implements InvocationHandler {
// 被代理的目标对象
private final Object target;
/**
* 构造函数,初始化目标对象。
*
* @param target 目标对象实例
*/
public NotNullProxy(Object target) {
this.target = target;
}
/**
* 创建一个代理实例。
*
* @param obj 需要被代理的对象
* @return 返回一个动态代理对象,该对象在方法调用时会进行参数非空检查
*/
public static Object newInstance(Object obj) {
return Proxy.newProxyInstance(
obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(),
new NotNullProxy(obj));
}
/**
* 在每次方法调用时执行的代理方法。
* 该方法会检查被@NotEmpty注解标记的参数是否为null。
*
* @param proxy 代理类实例
* @param method 正在被调用的方法
* @param args 方法参数
* @return 方法调用的返回值
* @throws Throwable 如果方法执行抛出异常
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Annotation[][] paramAnnotations = method.getParameterAnnotations();
if (args != null) {
for (int i = 0; i < args.length; i++) {
for (Annotation annotation : paramAnnotations[i]) {
if (annotation instanceof NotNull && args[i] == null) {
throw new IllegalArgumentException(((NotNull) annotation).message());
}
}
}
}
return method.invoke(target, args);
}
}
6.3、使用代理类
现在,我们可以创建原始对象的代理,并使用它来确保方法参数不为空。
public interface SomeService {
void someMethod(@NotNull String param);
}
public class SomeServiceImpl implements SomeService {
public void someMethod(String param) {
System.out.println("Executing someMethod with param: " + param);
}
}
public class Main {
public static void main(String[] args) {
SomeService service = new SomeServiceImpl();
SomeService proxy = (SomeService) NotNullProxy.newInstance(service);
proxy.someMethod(null); // 这会抛出异常
}
}
这个例子中,NotNullProxy
类拦截所有方法调用,并在实际调用目标方法前检查被 @NotNull
注解标记的参数。如果参数为 null,就抛出 IllegalArgumentException
。
7、编译时注解实现-生成字段 get 方法的注解
7.1、注解处理器
Java 注解处理器是一种工具,用于在编译时读取和处理注解信息,然后执行相应的代码生成、修改或其他处理任务。它们是 Java 编译器的一个扩展,使用注解处理器可以在不修改原始代码的情况下生成额外的源代码或编译时资源。
注解处理器运行在 Java 编译阶段,通过处理特定的注解来生成新的源文件、类文件或其他文件。它们主要用于以下目的:
- 代码生成:自动生成模板化的代码,减少重复代码编写;
- 编译时检查:在编译时进行额外的检查,确保代码符合特定规则,增强代码的健壳性和安全性;
- 编程框架开发:框架开发者可以创建注解和相应的处理器,简化使用者的代码实现。
7.2、使用方法
7.2.1、创建注解
首先定义一个或多个注解,这些注解将被用于标记代码中需要处理的部分。
package com.lizhengi.annotate.generateGetters;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author lizhengi
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GenerateGetters {
}
7.2.2、实现注解处理器
创建一个类来实现 javax.annotation.processing.Processor
接口。通常,可以继承 AbstractProcessor
类来简化实现。
package com.lizhengi.annotate.generateGetters;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.Element;
import javax.lang.model.element.VariableElement;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.element.ElementKind;
import javax.tools.Diagnostic;
/**
* 这个注解处理器会在编译时自动为带有 @GenerateGetters 注解的字段生成 getter 方法。
* 它只处理标记了 @GenerateGetters 的字段,并验证这些字段是否使用了该注解。
* 支持的 Java 源代码版本为 8。
*
* @author lizhengi
*/
@SupportedAnnotationTypes("com.lizhengi.annotate.generateGetters.GenerateGetters")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {
/**
* 处理每一个带有 @GenerateGetters 注解的元素。为被注解的字段生成 getter 方法,并将生成的代码作为示例打印出来。
*
* @param annotations 遇到的注解类型集合
* @param roundEnv 提供当前处理轮次环境的访问
* @return boolean 返回 true 表示这些注解已经被处理
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(GenerateGetters.class)) {
// 检查元素是否为字段
if (element.getKind() != ElementKind.FIELD) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@GenerateGetters 只适用于字段");
return true;
}
VariableElement varElement = (VariableElement) element;
String fieldName = varElement.getSimpleName().toString();
String fieldType = varElement.asType().toString();
String methodName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
String getter = generateGetter(fieldName, fieldType, methodName);
// 这里只是简单地打印生成的代码,实际情况下需要写入到文件中
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, getter);
}
return true;
}
/**
* 根据字段名称和类型生成 getter 方法的 Java 代码字符串。
*
* @param fieldName 字段名
* @param fieldType 字段类型
* @param methodName 方法名
* @return String 返回生成的 getter 方法代码
*/
private String generateGetter(String fieldName, String fieldType, String methodName) {
return "public " + fieldType + " " + methodName + "() {\n" +
" return this." + fieldName + ";\n" +
"}\n";
}
}
7.2.3、使用注解
在代码中使用注解:
package com.lizhengi.annotate.generateGetters;
/**
* @author lizhengi
*/
public class Person {
@GenerateGetters
private String name;
@GenerateGetters
private int age;
}
7.2.4、手动通过命令行编译
手动从命令行进行编译,并指定注解处理器,可以使用如下的命令:
javac -cp path/to/your/classes -processor your.package.GetterProcessor path/to/your/java/file/Person.java
例如我本地使用的:
javac -cp /Users/lizhengi/WorkProjects/lizhengi-java/out/production/lizhengi-java -processor com.lizhengi.annotate.generateGetters.GetterProcessor /Users/lizhengi/WorkProjects/lizhengi-java/src/com/lizhengi/annotate/generateGetters/Person.java
7.2.5、能否通过注解直接修改原文件
注解处理器通常用于生成新的文件,而不是修改已存在的源文件,所以直接修改原有的 Java 文件比较复杂,且不是标准做法。但你可以生成一个包含所需 getter 方法的新 Java 源文件,或者在某些情况下,使用特殊技术如 javapoet
库来帮助生成更结构化的代码。
Lombok 确实提供了一种在编译时自动插入代码(如 getter 和 setter 方法)到原有类中的能力,但它并不是通过修改原始 Java 源文件实现的。Lombok 使用了一种叫做 “annotation processing” 的技术来在编译阶段生成并注入代码,但其工作机制有所不同。
Lombok 的工作原理:
- 注解处理器:Lombok 是一个注解处理器,它在编译期间运行,捕捉到特定的注解(如
@Data
,@Getter
,@Setter
等)。 - AST(抽象语法树)修改:Lombok 实际上操作的是编译过程中的抽象语法树(AST)。AST 是源代码的树状表示,用于表示程序结构。Lombok 直接在这个树上添加、修改或生成新的节点。
- 字节码生成:通过修改 AST,Lombok 能在不触碰原始源文件的情况下,动态地添加字段、方法、构造函数等。当 AST 被编译成字节码时,这些添加就成为了类的一部分。
8、注解在 Java 生态中的应用
Java 注解作为一种强大的元数据机制,用于为 Java 代码添加补充信息,这些信息可以在编译时、加载时或运行时被读取,并影响程序的行为。注解在 Java 生态中有广泛的应用,主要包括以下几个方面:
-
依赖注入:在框架如 Spring 中,注解(如
@Autowired
,@Component
,@Service
等)简化了依赖管理和组件扫描,自动注入所需的依赖对象,减少了配置文件的复杂度; -
配置和元数据:注解用于配置和管理组件行为。例如,JPA(Java Persistence API)使用注解(如
@Entity
,@Table
,@Column
等)来配置 ORM(对象关系映射),定义实体类及其属性与数据库表之间的映射关系; -
AOP(面向切面编程):注解在 AOP 中广泛应用,用于定义切面和切点。例如,Spring AOP 使用
@Aspect
,@Before
,@After
等注解来标识切面类和方法,管理横切关注点; -
测试:JUnit 等测试框架使用注解(如
@Test
,@Before
,@After
等)来标识测试方法和生命周期回调方法,简化了单元测试的编写和管理; -
序列化和反序列化:如 Jackson 和 Gson 等 JSON 处理库使用注解(如
@JsonProperty
,@SerializedName
等)来控制对象与 JSON 数据的映射; -
安全和事务管理:注解(如
@Secured
,@Transactional
等)用于声明安全约束和事务边界,简化了安全检查和事务管理的配置; -
框架和工具支持:许多框架和工具使用注解来提供额外功能和配置支持,如 Swagger 用于 API 文档生成,Lombok 用于简化 Java 类的编写。
总的来说,注解在 Java 生态中通过简化配置、增强可读性和可维护性、支持元数据驱动的编程方式,极大地提升了开发效率和代码质量。