注解详解
1. 什么是注解
Java 注解又称 Java 标注,是 JDK5.0 引入的一种注释机制。
注解其实就是一种标记,常常用于代替冗余复杂的配置,它本身并不起任何作用,起作用的在于「注解的处理程序」。
注解处理程序通过捕获被注解标记的代码然后进行一些处理,这就是注解工作的方式。
1.1 自定义注解的使用场景
- 登录拦截
- AOP日志
- JSR303自定义校验
...
2. 如何定义注解
注解也是一种类的类型,他使用的修饰符为 @interface。在定义注解时,不能继承其他的注解或接口。
public @interface MyAnnotation {
}
// 接着我们就可以在类或者方法上作用我们刚刚新建的注解。
@MyAnnotation
public class test {
@MyTestAnnotation
public static void main(String[] args){
}
3. Java中的内置注解
Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。
作用于代码的注解:
@Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
@Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
@SuppressWarnings - 指示编译器去忽略注解中声明的警告。
元注解:作用在其他注解的注解。
@Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
@Documented - 标记这些注解是否包含在用户文档中。
@Target - 标记这个注解应该是哪种 Java 成员。
@Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
从 Java 7 开始,额外添加了 3 个注解:
@SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
@FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
@Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
3.1 元注解————@Retention
@Retention:指定标记注释的存储方式。
在@Retention注解中使用枚举RetentionPolicy来表示注解保留时期。
@Retention(RetentionPolicy.CLASS) // 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得。
@Retention(RetentionPolicy.SOURCE) // 注解仅存在于源码中,在class字节码文件中不包含。
@Retention(RetentionPolicy.RUNTIME) // 注解会在class字节码文件中存在,在运行时可以通过反射获取到。
//如果我们是自定义注解,我们自定义注解如果只存着源码中或者字节码文件中就无法发挥作用,而在运行期间能获取到注解才能实现我们目的,所以自定义注解中肯定是使用 @Retention(RetentionPolicy.RUNTIME)!!
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}
3.2 元注解————@Target
@Target:表示我们的注解作用的范围,可以是类,方法,方法参数变量等。
在@Target注解中使用枚举类ElementType表达作用类型。
@Target(ElementType.TYPE) // 作用接口、类、枚举、注解
@Target(ElementType.FIELD) // 作用属性字段、枚举的常量
@Target(ElementType.METHOD) // 作用方法
@Target(ElementType.PARAMETER) // 作用方法参数
@Target(ElementType.CONSTRUCTOR) // 作用构造函数
@Target(ElementType.LOCAL_VARIABLE) // 作用局部变量
@Target(ElementType.ANNOTATION_TYPE) // 作用于注解(@Retention注解中就使用该属性)
@Target(ElementType.PACKAGE) // 作用于包
@Target(ElementType.TYPE_PARAMETER) // 作用于类型泛型,即泛型方法、泛型类、泛型接口 (jdk1.8加入)
@Target(ElementType.TYPE_USE) // 类型使用.可以用于标注任意类型除了 class (jdk1.8加入)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
}
3.3 元注解————@Documented
@Document表示将注解中的元素包含到 Javadoc 中去。
3.4 元注解————@Inherited
如果被这个注解标记了,被标记的类、接口会继承父类、接口的上面的注解。
/**自定义注解*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyTestAnnotation {
}
/**父类标注自定义注解*/
@MyTestAnnotation
public class Father {
}
/**子类*/
public class Son extends Father {
}
/**测试子类获取父类自定义注解*/
public class test {
public static void main(String[] args){
//获取Son的class对象
Class<Son> sonClass = Son.class;
// 获取Son类上的注解MyTestAnnotation可以执行成功
MyTestAnnotation annotation = sonClass.getAnnotation(MyTestAnnotation.class);
}
}
3.5 元注解————@Repeatable
@Repeatable表示被这个元注解修饰的注解可以同时作用一个对象多次,且每次作用注解又可以代表不同的含义。
/**一个人喜欢玩游戏,他喜欢玩英雄联盟,绝地求生,极品飞车,尘埃4等,则我们需要定义一个人的注解,他属性代表喜欢玩游戏集合,一个游戏注解,游戏属性代表游戏名称*/
/**玩家注解*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface People {
Game[] value() ;
}
/**游戏注解*/
@Repeatable(People.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Game {
String value() default "";
}
/**玩游戏类*/
@Game(value = "LOL")
@Game(value = "PUBG")
@Game(value = "NFS")
@Game(value = "Dirt4")
public class PlayGame {
}
4. 注解的属性
注解自定义属性的格式为: 属性类型 属性名称();
可以通过default来声明参数的默认值。
注解中的变量都是成员变量(属性),属性名称就是使用注解括号中对应的参数名。属性类型就是注解括号中对应参数的类型。
如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可。
String message() default "";
4.1 注解属性类型
1.基本数据类型
2.String
3.枚举类型
4.注解类型
5.Class类型
6.以上类型的一维数组类型
4.2 为注解属性赋值
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyTestAnnotation {
String name() default "mao";
int age() default 18;
}
@MyTestAnnotation(name = "father",age = 50)
public class Father {
}
列子:
/**
* 水果颜色注解
* @author peida
*
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitColor {
/**
* 颜色枚举
* @author peida
*
*/
enum Color{ BULE,RED,GREEN};
/**
* 颜色属性
* @return
*/
Color fruitColor() default Color.GREEN;
}
public class Apple {
@FruitColor(fruitColor=Color.RED)
private String appleColor;
}
如果注解又多个属性,则可以在注解括号中用“,”号隔开分别给对应的属性赋值。
5. 解析注解
如何提取注解的属性,这才是使用注解的关键,获取属性的值才是使用注解的目的。
注解的本质是接口,该接口默认继承Annotation接口。
要获取注解相关的信息,必须通过Java的反射技术来获取 Annotation 对象,因为你除此之外没有别的获取注解对象的方法。
除此之外,Java在java.lang.reflect 包下新增了AnnotatedElement接口,该接口是(Class、Filed,Method和Constructor这几个类)的父接口。
AnnotatedElement常用方法如下:
// 判断该元素上是否配置有某个指定的注解
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
return GenericDeclaration.super.isAnnotationPresent(annotationClass);
}
// 获取该元素上指定的注解。之后再调用该注解的注解类型元素方法就可以获得配置时的值数据;
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
Objects.requireNonNull(annotationClass);
return (A) annotationData().annotations.get(annotationClass);
}
// 获得该对象身上配置的所有的注解。它会返回给我们一个注解数组,需要注意的是该数组的类型是Annotation类型,这个Annotation是一个来自于java.lang.annotation包的接口。
public Annotation[] getAnnotations() {
return AnnotationParser.toArray(annotationData().annotations);
}
// getDeclaredAnnotation(Class<T> annotationClass): 返回此元素上存在的指定类型的注释,如果该类型注释不存在,则返回 null
// getDeclaredAnnotations(): 返回直接存在于此元素上的所有注释,包括其继承的注释和重复注释
// getAnnotationByType(Class<T> annotationClass): 返回此元素上存在的指定类型的注释,如果存在多个注释,则返回重复注释
// getAnnotationsByType(Class<T> annotationClass): 返回此元素上存在的指定类型的注释,包括其继承的注释和重复注释
public class test {
public static void main(String[] args) throws NoSuchMethodException {
/**
* 获取类注解 属性
*/
Class<Father> fatherClass = Father.class;
boolean flag = fatherClass.isAnnotationPresent(MyTestAnnotation.class);
if(flag){
MyTestAnnotation annotation = fatherClass.getAnnotation(MyTestAnnotation.class);
System.out.println(annotation.name());
System.out.println(annotation.age());
}
/**
* 获取方法注解 属性
*/
try {
Field age = fatherClass.getDeclaredField("age");
boolean flag = age.isAnnotationPresent(Age.class);
if(flag){
Age annotation = age.getAnnotation(Age.class);
System.out.println(annotation.value());
}
Method play = PlayGame.class.getDeclaredMethod("play");
if (play!=null){
People annotation2 = play.getAnnotation(People.class);
Game[] value = annotation2.value();
for (Game game : value) {
System.out.println(game.value());
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
例子:
/***********注解声明***************/
/**
* 水果名称注解
* @author peida
*
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
String value() default "";
}
/**
* 水果颜色注解
* @author peida
*
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitColor {
/**
* 颜色枚举
* @author peida
*
*/
public enum Color{ BULE,RED,GREEN};
/**
* 颜色属性
* @return
*/
Color fruitColor() default Color.GREEN;
}
/**
* 水果供应者注解
* @author peida
*
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/**
* 供应商编号
* @return
*/
public int id() default -1;
/**
* 供应商名称
* @return
*/
public String name() default "";
/**
* 供应商地址
* @return
*/
public String address() default "";
}
/***********注解使用***************/
public class Apple {
@FruitName("Apple")
private String appleName;
@FruitColor(fruitColor=Color.RED)
private String appleColor;
@FruitProvider(id=1,name="陕西红富士集团",address="陕西省西安市延安路89号红富士大厦")
private String appleProvider;
public void setAppleColor(String appleColor) {
this.appleColor = appleColor;
}
public String getAppleColor() {
return appleColor;
}
public void setAppleName(String appleName) {
this.appleName = appleName;
}
public String getAppleName() {
return appleName;
}
public void setAppleProvider(String appleProvider) {
this.appleProvider = appleProvider;
}
public String getAppleProvider() {
return appleProvider;
}
public void displayName(){
System.out.println("水果的名字是:苹果");
}
}
/***********注解处理器***************/
public class FruitInfoUtil {
public static void getFruitInfo(Class<?> clazz){
String strFruitName=" 水果名称:";
String strFruitColor=" 水果颜色:";
String strFruitProvicer="供应商信息:";
Field[] fields = clazz.getDeclaredFields();
for(Field field :fields){
if(field.isAnnotationPresent(FruitName.class)){
FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
strFruitName=strFruitName+fruitName.value();
System.out.println(strFruitName);
}
else if(field.isAnnotationPresent(FruitColor.class)){
FruitColor fruitColor= (FruitColor) field.getAnnotation(FruitColor.class);
strFruitColor=strFruitColor+fruitColor.fruitColor().toString();
System.out.println(strFruitColor);
}
else if(field.isAnnotationPresent(FruitProvider.class)){
FruitProvider fruitProvider= (FruitProvider) field.getAnnotation(FruitProvider.class);
strFruitProvicer=" 供应商编号:"+fruitProvider.id()+" 供应商名称:"+fruitProvider.name()+" 供应商地址:"+fruitProvider.address();
System.out.println(strFruitProvicer);
}
}
}
}
/***********输出结果***************/
public class FruitRun {
/**
* @param args
*/
public static void main(String[] args) {
FruitInfoUtil.getFruitInfo(Apple.class);
}
}
====================================
水果名称:Apple
水果颜色:RED
供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延安路89号红富士大厦
6. 自定义注解+拦截器 实现登录校验
我们使用springboot拦截器实现这样一个功能,如果方法上加了@LoginRequired,则提示用户该接口需要登录才能访问,否则不需要登录。
6.1 先定义一个注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyField {
String description();
int length();
}
6.2 然后写两个简单的接口,访问sourceA,sourceB资源
@RestController
public class IndexController {
@GetMapping("/sourceA")
public String sourceA(){
return "你正在访问sourceA资源";
}
@GetMapping("/sourceB")
public String sourceB(){
return "你正在访问sourceB资源";
}
}
没添加拦截器之前成功访问:
// 实现spring的HandlerInterceptor 类先实现拦截器,但不拦截,只是简单打印日志,如下:
public class SourceAccessInterceptor implements HandlerInterceptor {
// 参数1:表示客户端发送的HTTP请求对象,可以用它来获取客户端提交的请求参数、请求头、请求方式等信息。
// 参数2:表示服务器返回的HTTP响应对象,可以用它来设置响应的状态码、响应头、响应内容等信息。
// 参数3:Object handler:表示被拦截的处理器对象,即处理该请求的控制器对象。可以通过该参数获取控制器对象的信息,例如控制器的类名、方法名等。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("进入拦截器了");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
// 实现spring类WebMvcConfigurer,创建配置类把拦截器添加到拦截器链中。
@Configuration
public class InterceptorTrainConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SourceAccessInterceptor()).addPathPatterns("/**");
}
}
拦截成功如下:
6.3 在sourceB方法上添加我们的登录注解@LoginRequired
@RestController
public class IndexController {
@GetMapping("/sourceA")
public String sourceA(){
return "你正在访问sourceA资源";
}
@LoginRequired
@GetMapping("/sourceB")
public String sourceB(){
return "你正在访问sourceB资源";
}
}
// 参数1:表示客户端发送的HTTP请求对象,可以用它来获取客户端提交的请求参数、请求头、请求方式等信息。
// 参数2:表示服务器返回的HTTP响应对象,可以用它来设置响应的状态码、响应头、响应内容等信息。
// 参数3:Object handler:表示被拦截的处理器对象,即处理该请求的控制器对象。可以通过该参数获取控制器对象的信息,例如控制器的类名、方法名等。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("进入拦截器了");
// HandlerMethod: 是Spring MVC框架中的一个类,它表示一个具体的处理请求的方法。HandlerMethod包含了处理请求的控制器对象、处理请求的方法、方法的参数等信息。
// 属性:
// Object bean:表示处理该请求的控制器对象。
// Method method:表示处理该请求的方法。
// MethodParameter[] methodParameters:表示处理该请求的方法的参数列表。
// 反射获取方法上的LoginRequred注解
HandlerMethod handlerMethod = (HandlerMethod)handler;
LoginRequired loginRequired = handlerMethod.getMethod().getAnnotation(LoginRequired.class);
if(loginRequired == null){
return true;
}
// 有LoginRequired注解说明需要登录,提示用户登录
// 在这里实现登录逻辑,已经登录的就返回true,否则false。
return false;
}
运行成功,访问sourceB时需要登录了,访问sourceA则不用登录。
7. 自定义注解+AOP 实现日志打印
7.1 先导入切面需要的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
7.2 定义一个注解@MyLog
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
7.3 定义一个切面类,见如下代码注释理解:
@Aspect // 1.表明这是一个切面类
@Component
public class MyLogAspect {
// 2. PointCut表示这是一个切点,@annotation表示这个切点切到一个注解上,后面带该注解的全类名
// 切面最主要的就是切点,所有的故事都围绕切点发生
// logPointCut()代表切点名称
@Pointcut("@annotation(me.zebin.demo.annotationdemo.aoplog.MyLog)")
public void logPointCut(){};
// 3. 环绕通知
// ProceedingJoinPoint是Spring框架中AOP(面向切面编程)的一个重要概念,表示正在被通知(Advice)的连接点(Join Point),包括被通知方法的相关信息以及方法调用的上下文信息。
// 在AOP中,ProceedingJoinPoint通常用于实现环绕通知。
// ProceedingJoinPoint类的主要方法包括:
// Object[] getArgs():获取被通知方法的参数列表。
// Object proceed():继续执行被通知方法。
// Signature getSignature():获取被通知方法的签名。
// Object getTarget():获取被通知方法所属的目标对象。
// Object getThis():获取代理对象。
@Around("logPointCut()")
public void logAround(ProceedingJoinPoint joinPoint){
// 获取方法名称
String methodName = joinPoint.getSignature().getName();
// 获取入参
Object[] param = joinPoint.getArgs();
StringBuilder sb = new StringBuilder();
for(Object o : param){
sb.append(o + "; ");
}
System.out.println("进入[" + methodName + "]方法,参数为:" + sb.toString());
// 继续执行方法
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println(methodName + "方法执行结束");
}
}
7.4 在步骤二中的IndexController写一个sourceC进行测试,加上我们的自定义注解:
@MyLog
@GetMapping("/sourceC/{source_name}")
public String sourceC(@PathVariable("source_name") String sourceName){
return "你正在访问sourceC资源";
}
启动springboot web项目,输入访问地址: