自定义注解及应用场景
——深圳蜗牛学苑
课程目标
- 理解自定义注解概念
- 掌握自定义注解语法
- 自定义注解场景中的使用
- 熟练掌握自定义注解+AOP的使用方式
一、自定义注解
我们回顾一下方法的重写,会发现有@Override,我们把它称为重写的注解。那么注解到底什么呢?我们能不能自定义注解。
1、概念:
注解是一种能被添加到java代码中的元数据,类、方法、变量、参数和包都可以用注解来修饰。注解对于它所修饰的代码并没有直接的影响。
* 注解是一种元数据形式。即注解是属于java的一种数据类型,和类、接口、数组、枚举类似。
* 注解用来修饰,类、方法、变量、参数、包。
* 注解不会对所修饰的代码产生直接的影响。
既然注解是一种数据类型,那我们自然可以去自定义的使用它
2、自定义注解的语法
简单来说自定义注解分为三步:
注解在Java中,与类、接口、枚举类似,因此其声明语法基本一致,只是所使用的关键字有所不同@interface
。
在底层实现上,所有定义的注解都会自动继承java.lang.annotation.Annotation接口。
public @interface TestAnnotation {
}
根据我们在自定义类的经验,在类的实现部分无非就是书写构造、属性或方法。但是,在自定义注解中,其实现部分只能定义一个东西:注解类型元素(annotation type element)
/**
* 自定义注解 @interface
*/
public @interface TestAnnotation {
//定义注解类型元素
public String name();
int age() default 18; //default代表带有默认值为18
int[] array();
}
注解定义就只包括了上面的两部分内容:
定义注解类型元素时需要注意如下几点:
- 访问修饰符必须为public,不写默认为public;
- 该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型(体现了注解的嵌套效果)以及上述类型的一位数组;
- 该元素的名称一般定义为名词,如果注解中只有一个元素,请把名字起为value(后面使用会带来便利操作);
- ()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法;
- default代表默认值,值必须和第2点定义的类型一致;
- 如果没有默认值,代表后续使用注解时必须给该类型元素赋值。
除了定义好注解,在哪用?怎么用?注意些什么?说这些之前,我们还需要补充一个概念:元注解
3、元注解
(1) @Target:是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的
public enum ElementType {
/** 类,接口(包括注解类型)或枚举的声明 */
TYPE,
/** 属性的声明 */
FIELD,
/** 方法的声明 */
METHOD,
/** 方法形式参数声明 */
PARAMETER,
/** 构造方法的声明 */
CONSTRUCTOR,
/** 局部变量声明 */
LOCAL_VARIABLE,
/** 注解类型声明 */
ANNOTATION_TYPE,
/** 包的声明 */
PACKAGE
}
//@CherryAnnotation被限定只能使用在类、接口或方法上面
@Target(value = {ElementType.TYPE,ElementType.METHOD})
public @interface TestAnnotation {
String name();
int age() default 18;
int[] array();
}
(2)@Retention注解,翻译为持久力、保持力。即用来修饰自定义注解的生命力。
注解的生命周期有三个阶段:1、Java源文件阶段;2、编译到class文件阶段->默认;3、运行期阶段。
同样使用了RetentionPolicy枚举类型定义了三个阶段:
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
* (注解将被编译器忽略掉)
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
* (注解将被编译器记录在class文件中,但在运行时不会被虚拟机保留,这是一个默认的行为)
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
* (注解将被编译器记录在class文件中,而且在运行时会被虚拟机保留,因此它们能通过反射被读取到)
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
- 如果一个注解被定义为RetentionPolicy.SOURCE,则它将被限定在Java源文件中,那么这个注解即不会参与编译也不会在运行期起任何作用,这个注解就和一个注释是一样的效果,只能被阅读Java文件的人看到;
- 如果一个注解被定义为RetentionPolicy.CLASS,则它将被编译到Class文件中,那么编译器可以在编译时根据注解做一些处理动作,但是运行时JVM(Java虚拟机)会忽略它,我们在运行期也不能读取到;
- 如果一个注解被定义为RetentionPolicy.RUNTIME,那么这个注解可以在运行期的加载阶段被加载到Class对象中。那么在程序运行阶段,我们可以通过反射得到这个注解,并通过判断是否有这个注解或这个注解中属性的值,从而执行不同的程序代码段。我们实际开发中的自定义注解几乎都是使用的RetentionPolicy.RUNTIME;
- 在默认的情况下,自定义注解是使用的RetentionPolicy.CLASS。
(3)@Documented,是被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。
(4)@Inherited注解,是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解。@Inherited注解只对那些@Target被定义 为ElementType.TYPE的自定义注解起作用。
说完了元注解,我们可以看到元注解其实就是用来配置我们的自定义注解,让自定义注解更加精确的使用
8、自定义注解补充
@Retention(RetentionPolicy.RUNTIME) //规定运行时
@Target(value = {ElementType.METHOD}) //规定在方法上使用
@Documented//JavaDoc文档
public @interface CherryAnnotation {
String name();
int age() default 18;
int[] score();
}
简单分析下:
- CherryAnnotation的@Target定义为ElementType.METHOD,那么它书写的位置应该在方法定义的上方
- 由于我们在CherryAnnotation中定义的有注解类型元素,而且有些元素是没有默认值的,这要求我们在使用的时候必须在标记名后面打上(),并且在()内以“元素名=元素值“的形式挨个填上所有没有默认值的注解类型元素(有默认值的也可以填上重新赋值),中间用“,”号分割;
9、使用注解
public class Student {
@CherryAnnotation(name = "cherry-peng",age = 23,score = {99,66,77})
public void study(int times){
for(int i = 0; i < times; i++){
System.out.println("Good Good Study, Day Day Up!");
}
}
}
4、自定义注解的value成员:
(1)如果注解本身没有注解类型元素,那么在使用注解的时候可以省略(),直接写为:@注解名,它和标准语法@注解名()等效!
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface FirstAnnotation {
}
//等效于@FirstAnnotation()
@FirstAnnotation
public class JavaBean{
//省略实现部分
}
(2)如果注解本本身只有一个注解类型元素,而且命名为value,那么在使用注解的时候可以直接使用:@注解名(注解值),其等效于:@注解名(value = 注解值)
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface SecondAnnotation {
String value();
}
//等效于@ SecondAnnotation(value = "this is second annotation")
@SecondAnnotation("this is annotation")
public class JavaBean{
//省略实现部分
}
(3)如果注解中的某个注解类型元素是一个数组类型,在使用时又出现只需要填入一个值的情况,那么在使用注解时可以直接写为:@注解名(类型名 = 类型值),它和标准写法:@注解名(类型名 = {类型值})等效!
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface ThirdAnnotation {
String[] name();
}
//等效于@ ThirdAnnotation(name = {"this is third annotation"})
@ThirdAnnotation(name = "this is third annotation")
public class JavaBean{
//省略实现部分
}
二、自定义注解应用场景
1、自定义注解的使用场景很多,我们在造轮子写框架的过程经常会使用到。在我们平时的项目场景中,也可以随处可以看到自定义注解可以发挥作用的场景,例如:
1)全局日志的打印
2)用户权限的校验
3)登录的校验
4)数据脱敏
等等场景。
2、而在我们后面的微服务阶段,自定义注解也可以用来测试和熔断服务。
我们也在这里列举出几个业务场景,来模拟一下自定义注解在我们实际项目场景中的使用:
1、业务场景1:权限校验
自定义注解经常和springAOP一起配合使用,能让aop的增强更加轻松和优雅,消除冗余代码。例如日志,登录校验,权限校验等。
(1)首先我们定义了一个缓存类CacheManager来模拟保存两个用户zhangsan和lisi,并给予他们一定的角色
package com.woniu.test4;
import java.util.*;
public class CacheManager {
//初始化一个map集合来存储用户名和该用户所拥有的的角色
public static Map<String, List<String>> USER_ROLES = new HashMap<>();
static {
//初始化一个list集合来存储角色
List<String> roles = new ArrayList<>();
roles.add("admin");
roles.add("user");
List<String> roles2 = new ArrayList<>();
roles2.add("user");
USER_ROLES.put("zhangsan",roles);
USER_ROLES.put("lisi",roles2);
}
}
(2)controller 层中设计删除,新增,查询方法,我们将要实现使得这些方法拥有一些角色限定,如果没有该角色的用户,是无法执行该方法。
@RestController
public class UserController2 {
@GetMapping("/deleteUser")
public String deleteUser(){
return "执行了删除用户的方法!";
}
@GetMapping("/addUser")
public String addUser(){
return "执行了新增用户的方法!";
}
@GetMapping("/list")
public String list(){
return "执行了查询用户的方法!";
}
}
(3)首先定义一个 自定义注解HasRole,并且保存一个成员value来存储角色名称
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HasRole {
//存储角色名称
String value();
}
(3)在controller上面增加自定义注解,并且表名该方法所具有的权限名称
@RestController
public class UserController2 {
@HasRole("admin")
@GetMapping("/deleteUser")
public String deleteUser(){
return "执行了删除用户的方法!";
}
@HasRole("user")
@GetMapping("/addUser")
public String addUser(){
return "执行了新增用户的方法!";
}
@GetMapping("/list")
public String list(){
return "执行了查询用户的方法!";
}
}
(4)通过springAOP 来定义一个切面类,并且使用前置通知,获取用户名称从而来判定该用户是否具有响应的权限
@Aspect //定义切面类
@Component
public class AppConfig {
//定义切点
@Pointcut("@annotation(com.woniu.test4.HasRole)")
public void pointcut(){}
//前置通知
@Before("pointcut()")
public void before(JoinPoint joinPoint){
//通过RequestContextHolder来获得ServletRequestAttributes
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//通过ServletRequestAttributes来获得请求对象request
HttpServletRequest request = requestAttributes.getRequest();
String username = request.getParameter("username");
//获取当前角色的权限集合
List<String> strings = CacheManager.USER_ROLES.get(username);
//获取当前方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//反射获取当前方法对象
Method method = signature.getMethod();
//获取当前方法头顶的注解
HasRole hasRole = method.getDeclaredAnnotation(HasRole.class);
//判断是否为空
if(hasRole!=null&&(strings==null || !strings.contains(hasRole.value()))){
throw new RuntimeException("用户没有访问权限!");
}
}
}
(5)测试
当我们使用lisi用户时,李四用户只有user角色,所以是不能执行delete方法,报出异常
如果,我们传入的用户名为zhangsan,则具有admin权限,页面正常展示。
这样我们就用自定义注解来实现了角色权限的简单校验,当然,直接报出异常的方式不是很友好,我们只需要写出一个全局异常处理就能友好的解决了。
2、业务场景2:敏感数据的脱敏
在很多项目中,存储了大量的用户敏感信息,而这些信息如果直接显示在网页上,是具有很大的安全问题,所以我们需要在数据向网页展示时,先经过脱敏的处理。
所谓脱敏其实就是敏感数据的半隐藏,例如身份证,手机号,用户名等等。
(1)首先我们准备好一个实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
// 用户手机号
private String userPhone;
// 用户身份证号
private String id_card;
}
(2)在controller层初始化一个对象,并且将信息返回
@RestController
public class UserController {
@GetMapping("/user2")
public User getUserInfo(){
return new User("13239687323","6229923199910010014");
}
}
此时测试,数据是会完整的展现在页面上
那么此处,我们同样可以使用springAOP+自定义注解来获取到属性中的数据,并且手动脱敏,这次我们可以尝试一下不一样的写法。
2.1 认识 @JsonSerialize注解的使用
在开发中,我们将对象序列化为JSON传输给前端,有时候我们的某个或者某些字段需要特殊处理。
比如我们有一个日期字段,我们希望当日期为NULL时给前端不返回NULL而返回为未完成等信息,或者我们有一些状态字段,我们需要给前端返回状态对应的信息,此时我们就需要自定义字段的序列化。这就是@JsonSerialize的用处
第一步,我们先写一个指定的处理类
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.Date;
//该类需要继承JsonSerializer
public class DateJsonSerialize extends JsonSerializer<Date> {
//重写serialize方法
@Override
public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
//自定义处理方式
jsonGenerator.writeString("未填写");
}
}
第二步,在此字段上加上@JsonSerialize注解,并且使用nullsUsing属性,即当此字段为NULL时就使用我们自定义的解析类序列化此字段(注意当此字段不为NULL时不执行此配置)
import java.util.Date;
public class Student {
private String stuName;
private Integer stuAge;
//当该属性为null时,会触发json的序列化
@JsonSerialize(nullsUsing = DateJsonSerialize.class)
private Date birthday;
}
测试
既然JsonSerialize可以帮助我们重新序列化属性数据,我们正好可以使用这个机制,配合自定义注解,来完成我们数据脱敏
2.2 重新封装一个SensitiveJsonSerializer类来处理user的数据脱敏
public class SensitiveJsonSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
//手动脱敏
String substring = value.substring(value.length() - 4);
value = "***"+ substring;
gen.writeString(value);
}
2.3 封装出自定义注解
//自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside //这个注解用来标记Jackson复合注解,当你使用多个Jackson注解组合成一个自定义注解时会用到它
@JsonSerialize(using = SensitiveJsonSerializer.class) //指定使用自定义的序列化器
public @interface Sensitive {
}
@JsonSerialize来指明我们自己的序列化器
当我们自定义的注解要和Jackson注解组合时,需要标注@JacksonAnnotationsInside,来表明这是一个符合注解
测试
2.4 优化代码
现在已经实现出了我们的效果,但是我们的代码仍可以做进一步的优化,因为现在数据脱敏的格式比较单一,而且如果当需要脱敏的规则改变或者需要脱敏的属性增加时,我们仍然需要回去修改源代码,不如我们用一个枚举enum来专门存储所有的脱敏规则
2.4.1 新建一个enum类SensitiveStrategy
在这个枚举中,我们需要定义出各种类型属性的脱敏规则
思考—enum中,成员的类型应该是什么?
既然每个成员除了脱敏规则不一样,脱敏的流程都是一样的,那我们就可以设计成jdk8中Function<T,R>作为成员类型,这样我们就可以使用Fuction中的apply方法来消除掉重复的调用脱敏方法的代码。
public enum SensitiveStrategy {
//常量成员
//正则表达式来存储脱敏规则,格式规定为:132***7323
USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
ID_CARD(s -> s.replaceAll("(\\d{3})\\d{13}(\\w{2})", "$1****$2")),
PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
ADDRESS(s -> s.replaceAll("(\\S{3})\\S{2}(\\S*)\\S{2}", "$1****$2****"));
//构造方法和get/set
private Function<String, String> desensitizes;
SensitiveStrategy(Function<String, String> desensitizes) {
this.desensitizes = desensitizes;
}
public Function<String, String> getDesensitizes() {
return desensitizes;
}
public void setDesensitizes(Function<String, String> desensitizes) {
this.desensitizes = desensitizes;
}
}
提示:正则表达式,大家只需要看懂即可,不需要记忆~
2.4.2 优化SensitiveJsonSerializer类
既然脱敏的代码封装到枚举中,那在SensitiveJsonSerializer类的serialize序列化方法中,只需要调用一下function的apply方法即可
public class SensitiveJsonSerializer extends JsonSerializer<String> {
//提供枚举成员属性
private SensitiveStrategy strategy;
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
//调用enum中function的apply方法,即 脱敏方法,传入原数据
gen.writeString(strategy.getDesensitizes().apply(value));
}
}
2.4.3 优化自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside //这个注解用来标记Jackson复合注解,当你使用多个Jackson注解组合成一个自定义注解时会用到它
@JsonSerialize(using = SensitiveJsonSerializer.class) //指定使用自定义的序列化器
public @interface Sensitive {
SensitiveStrategy value(); //该自定义注解需要的参数 strategy-参数名称 SensitiveStrategy-参数类型
}
在自定义注解中加入enum的成员属性
原因:之前我们是统一将数据脱敏程 ***XXX的格式,这样显然和真实场景不同。加入strategy成员,来标注出该属性需要走哪个类型的脱敏,从而让枚举自动识别出来并且进行脱敏
2.4.4 实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
// 用户手机号
@Sensitive(SensitiveStrategy.PHONE)
private String userPhone;
// 用户身份证号
@Sensitive(SensitiveStrategy.ID_CARD)
private String id_card;
}
测试,发现报出NullPointer异常,debug一下,不难发现如下问题:
我们需要获取到属性头顶的注解对象,并且赋值到这个属性中,才可以正确执行
//ContextualSerializer是 Jackson 提供的另一个序列化相关的接口,它的作用是通过字段已知的上下文信息定制JsonSerializer,只需要实现createContextual方法即可
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer{
private SensitiveStrategy strategy;
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
//调用fuction apply方法,传入数据
gen.writeString(strategy.getDesensitizes().apply(value));
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
// 通过反射获取注解对象
Sensitive annotation = property.getAnnotation(Sensitive.class);
if (Objects.nonNull(annotation)) {
this.strategy = annotation.value();
return this;
}
//如果为null,则说明没有添加注解对象,则返回默认的序列化的对象即可
return prov.findValueSerializer(property.getType(), property);
}
}
测试
3、业务场景3:全局日志
3.1 封装记录日志实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MaterialEamLog {
private String params; //参数
private String method; //方法
private String result; //返回结果
private String url; //URL地址
private String ip; // ip地址
private String time; //执行时间
}
3.2 定义serviceImpl层(模拟入库)
@Service
public class IMaterialEamLogService {
public boolean insert(MaterialEamLog materialEamLog){
System.out.println(materialEamLog);
System.out.println("执行新增方法,将日志入库");
return true;
}
}
3.3 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RequiredLog {
}
3.4 定义AOP切面类
@Aspect
@Component
@Slf4j
public class SysLogAspect {
@Autowired
private IMaterialEamLogService materialEamLogService;
/**
* @Pointcut 注解用于描述或定义一个切入点
* 切入点的定义需要遵循spring中指定的表达式规范
*/
@Pointcut("@annotation(com.woniu.test6.RequiredLog)")
public void logPointCut() {}
/**
* @Around 注解描述的方法为一个环绕通知方法,
* 在此方法中可以添加扩展业务逻辑,可以调用下一个
切面对象或目标方法
* @param jp 连接点(此连接点只应用@Around描述的方法)
* @return
* @throws Throwable
*/
@Around("logPointCut()")
public Object aroundAdvice(ProceedingJoinPoint jp)
throws Throwable{
long start=System.currentTimeMillis();
log.info("start:"+start);
try {
//调用下一个切面或目标方法
result = jp.proceed();
}finally {
long end=System.currentTimeMillis();
log.info("end:"+end);
//记录日志(用户行为信息)
saveLog(jp,result,String.valueOf(end-start));
}
return result;
}
//日志记录
private void saveLog(ProceedingJoinPoint jp,Object result,String time) {
//1.获取用户行为日志(ip,username,operation,method,params,time,createdTime)
//获取类的字节码对象,通过字节码对象获取方法信息
Class<?> targetCls=jp.getTarget().getClass();
//获取方法签名(通过此签名获取目标方法信息)
MethodSignature ms=(MethodSignature)jp.getSignature();
//获取目标方法名(目标类型+方法名)
String targetClsName=targetCls.getName();
String targetObjectMethodName=targetClsName+"."+ms.getName();
//获取请求参数
String targetMethodParams= Arrays.toString(jp.getArgs());
String url = "";
String ipAddr = "";
if(RequestContextHolder.getRequestAttributes() != null){
//获取请求url
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
url = request.getRequestURL().toString();
ipAddr = request.getRemoteHost();
}
//2.封装用户行为日志(SysLog)
//插入日志--start
MaterialEamLog log = new MaterialEamLog();
log.setParams(targetMethodParams);
log.setMethod(targetObjectMethodName);
//获取注解中的参数
log.setResult(String.valueOf(result));
log.setUrl(url);
log.setIp(ipAddr);
log.setTime(time);
//3.调用业务层对象方法(saveObject)将日志写入到数据库
materialEamLogService.insert(log);
}
3.5 测试