一、注解及其原理
1.注解的基本概念
注解,可以看作是对 一个类/方法的一个扩展的模版,每个类/方法按照注解类中的规则,来为类/方法注解不同的参数,在用到的地方可以得到不同的类/方法中注解的各种参数与值。
从JDK5开始,Java增加了对元数据(描述数据属性的信息)的支持。其实说白就是代码里的特殊标志,这些标志可以在编译、类加载和运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。
2.标准注解与元注解
2.1.标准注解
2.1.1.@Override
定义在 java.lang.Override中,此注解只能作用于方法,表示一个方法声明打算重写超类中的另一个方法声明,简单来说就是子类在重写父类方法的时候可以加上这个注解。默认情况下,子类重写的方法会自动覆盖父类的方法,但经验告诉我们,必须显式地在子类重写父类的方法上添加@Override注解来检查并标记子类是否重写了父类的方法。
2.1.2.@Deprecated
定义在java.lang.Deprecated中,此注解可以作用于方法、属性 和类等等,表示不建议程序员使用被@Deprecated所作用的对象,通常是因为它很危险或者存在更好的选择,即此对象已经过时,不推荐使用。例如:
- 作用在方法上
- 作用在类上
2.1.3.@SuppressWarnings
定义在 java.lang.SuppressWarnings 中,用来抑制编译时的警告信息,与前两个注释有所不同,你需要添加一个参数才能正确使用,这些参数都是已经定义好了的, 我们选择性的使用就好了,一般我们使用@SuppressWarnings(“all”)。
2.2.元注解
除了 Java 中为我们定义好的注解,我们还可以通过元注解来自定义注解, Java定义了4个标准的元注解(meta-annotation),用来定义和描述其他注解(自定义注解),所以也称为元数据注解。
2.2.1.@Target
用来描述注解的作用对象,即注解可以使用在什么地方,在定义注解的时候使用该注解可以清晰地知道它的使用范围,它的取值范围被定义在了一个枚举类中,常见的枚举值包括:
- ElemenetType.CONSTRUCTOR 构造器声明 ;
- ElemenetType.FIELD 域声明(包括 enum 实例);
- ElemenetType.LOCAL_VARIABLE 局部变量声明;
- ElemenetType.METHOD 方法声明;
- ElemenetType.PACKAGE 包声明;
- ElemenetType.PARAMETER 参数声明;
- ElemenetType.TYPE 类,接口(包括注解类型)或enum声明;
2.2.2.@Retention
用来描述在什么级别保存该注解信息。可选的参数值在枚举类型 RetentionPolicy中,包括:
- RetentionPolicy.SOURCE 注解将被编译器丢弃;
- RetentionPolicy.CLASS 注解在class文件中可用,但会被VM丢弃;
- RetentionPolicy.RUNTIME VM将在运行期也保留注解,因此可以通过反射工具读取注解的信息。
2.2.3.@Documented
将此注解包含在javadoc中,它代表着此注解会被javadoc工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同。相当与@see,@param等。
2.2.4.@Inherited
表示允许子类继承父类中的注解。
3.自定义注解
3.1.通过元注解定义注解
通过元注解,我们可以定义新的注解。长久以来,注解被视为一种轻量级的配置化技术方案,与XML相比,注解更加简单、便捷,但注解一般应用于比较简单的参数配置,而对于复杂的参数,就必须借助XML这个工具了。
3.1.1.定义注解及属性声明
- 使用@interface关键字来定义新的注解。例如:
注解和类一样,也可以声明自己的属性,用来对注解自身进行描述。这样,原来写在配置文件中的信息,就可以通过注解的属性进行描述。
1)定义注解的必录属性:String name(),在使用注解时,通过name=xxx来设置属性name的值,若没有设置属性name的值,系统会提示编译异常。
2)定义注解属性的默认值:String name() default “test”,在使用注解时,若没有设置属性name的值,系统会自动将注解的name属性设置为默认值"test"。
3)特殊属性value:如果注解@Query中只定义了一个名称为value的必录属性,那么使用注解时可以省略"value=",如@Query (“xxx”)。
3.1.2.定义不包含属性的注解
这类注解内部不存在任何属性,因此仅仅是起到一个标记的作用(与标记接口类似)。下面是一个不包含属性的自定义注解示例:
3.1.3.定义包含属性的注解
注解还可以定义自己的属性,这些属性可以用来存储一些关键、特征信息,以便程序通过当前注解的属性存储的信息来处理相应的业务逻辑。下面是一个包含属性的自定义注解示例:
3.1.3.1.注解属性支持的数据类型
注解的属性与类的属性除了声明方式存在差异外,还体现在所支持的数据类型。注解属性支持的数据类型包含以下几种:
- 所有基本数据类型;
- 字符串类型String;
- 枚举类型enum;
- 注解类型Annotation;
- 以上数据类型的数组类型;
例如:
注意:如果你使用了其他数据类型,就会提示编译异常。
3.2.注解的使用
在上文中,我们定义了方法注解,因此可以作为方法级的轻量级配置使用,具体使用方式如下所示:
二、Spring注解原理剖析
Spring注解是自定义注解在实际开发中的最佳实践之一,一般而言,我接触到的Spring注解——@Controller、@Service和@Autowired等注解都是运行时注解,因此底层都是基于Java反射实现的。下面我们通过一个实际案例来深入理解Spring框架中注解的底层实现原理。
1.自定义自动注入注解思路点拨
思路点拨:由于Service类应用于Controller类中,因此,可以通过切面拦截Controller类,而切面无法拦截类上的注解,只能拦截方法上的注解,拦截到目标类的所有方法后,就遍历Controller类的所有属性,并判断属性上是否标记了@AutoInjected注解,再通过反射将实现类的实例注入到该属性中。
2.自定义@AutoInjected注解
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoInjected {
}
3.创建AutoInjectedAspect切面
import com.zh.test.annotation.AutoInjected;
import com.zh.test.system.utils.ApplicationUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import java.lang.reflect.Field;
@Aspect
@Component
public class AutoInjectedAcpect {
@Pointcut("execution(* com.zh.test..*.controller..*.*(..))")
public void beforeDoPointcut() {
}
@Before("beforeDoPointcut()")
public void beforeDo(JoinPoint joinPoint) {
Object target = joinPoint.getTarget();
// 判断目标类是否标记了@RestController或@Controller注解
RestController restController = AnnotationUtils.findAnnotation(target.getClass(), RestController.class);
if(restController == null) {
Controller controller = AnnotationUtils.findAnnotation(target.getClass(), Controller.class);
if(controller == null) {
return;
}
}
Field[] fields = target.getClass().getDeclaredFields();
for(Field field : fields) {
// 获取属性上的注解
AutoInjected autoInjected = field.getAnnotation(AutoInjected.class);
// 如果为空,说明该属性上没有标记@AutoInjected注解,不予处理
if(autoInjected == null) {
continue;
}
Class<?> fieldType = field.getType();
// 获取属性对应接口类型的实现类的实例
Object bean = ApplicationUtil.getBean(fieldType);
try {
field.setAccessible(true);
// 将属性对应接口类型的实现类的实例属注入到属性中
field.set(target, bean);
} catch (IllegalAccessException e) {
throw new RuntimeException(e.getMessage());
}
}
}
}
4.@AutoInjected注解的应用
import com.zh.test.annotation.AutoInjected;
import com.zh.test.system.entity.SysPageMeta;
import com.zh.test.system.service.IDemoService;
import com.zh.test.system.service.IMetaService;
import com.zh.test.system.service.impl.DemoServiceImpl;
import com.zh.test.system.utils.ApplicationUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.lang.reflect.Field;
import java.util.Map;
@RequestMapping("/demo")
@RestController
public class DemoController {
@AutoInjected
private IDemoService service;
@AutoInjected
private IMetaService metaService;
@GetMapping("/queryByPage")
public Map<String, Object> queryByPage() {
Map<String, Object> result = service.queryByPage();
return result;
}
@PostMapping
public Boolean addMetadata() {
boolean result = metaService.addMetadata(new SysPageMeta());
return result;
}
}
三、注解的实际应用
在第一章中,我们自定义的注解都是运行时注解,因此可以通过反射工具获取注解信息。不同的作用对象,获取注解的方式也不相同,下面我们举例说明。
1.通过切面实现自定义(方法)注解
当注解定义在类或方法上时,可以通过切面拦截并获取注解实例。在SpringBoot项目中,如果要使用切面,就必须引入以下的jar包依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
可以通过注解@Aspect和@Component来定义一个切面类:
具体示例如下:
@Aspect
@Component
public class QueryAspect {
@Before("@annotation(query)")
public void beforeQuery(JoinPoint joinPoint, Query query) {
// 数据库
String dbName = query.dbName();
// 表名
String tableName = query.tableName();
// 是否分页
boolean paginate = query.paginate();
// 字段信息
Column[] columns = query.columns();
System.out.println("dbName:" + dbName);
System.out.println("tableName:" + tableName);
System.out.println("paginate:" + paginate);
System.out.println("columns:" + Arrays.toString(columns));
}
}
注意:当切面的切点表达式为@annotation(xxx)时,目标注解必须作用于目标对象的方法上,否则切面将无法拦截到目标注解!
2.通过ConstraintValidator校验器自定义方法参数注解
当注解定义在方法参数上时,无法通过切面拦截并获取注解实例,此时就必须借助ConstraintValidator校验器来获取注解信息。示例如下:
- 引入hibernate-validator.jar包依赖
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.0.Final</version>
</dependency>
- 定义用于辅助校验的注解
例子如下:
// 注解作用对象为类和方法参数
@Target({ElementType.TYPE, ElementType.PARAMETER})
// 运行时类型的注解,可通过反射工具获取当前注解实例
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExtCheck {
String message() default "非法扩展字段";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
注意:辅助校验的注解必须至少包含message、groups和payload三个最基本的成员属性,否则将抛出运行时异常。
- 实现ConstraintValidator接口
例子如下:
- 在@ExtCheck注解上添加上述实现的ConstraintValidator校验器
例子如下:
- 定义全局异常处理器
例子如下:
import com.zh.test.exception.ExtException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* 全局异常处理器
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ExtException.class)
public String handleAccessDeniedException(ExtException e, HttpServletRequest request) {
return e.getMessage();
}
}
public class ExtException extends RuntimeException {
public ExtException() {
super();
}
public ExtException(String message) {
super(message);
}
}
- 在Controller层的类或方法参数前添加@Validated(必须)和@ExtCheck注解