前言
在工作中经常发现,我们经常会使用一些spring体系的注解。如果面试的时候,你跟老板说你会使用注解,老板觉得你这个人还行;但是如果你和老板说你会自定义注解解决问题,老板肯定就会眼前一亮,这是个人才鸭,嗯,小伙子20k够不够…
学习目标
1)自定义一个注解,搭配aop实现一个日志打印功能
2)结合案例,对注解应用深入了解
自定义注解实现
准备工作
先创建一个springboot项目,并引入aop相关依赖。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.9</version> </dependency> </dependencies>
项目启动端口配置为8081
server.port=8081
创建一个注解类
import java.lang.annotation.*; /** * 自定义注解: TestLog * */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface TestLog { String value() ; }
自定义注解使用关键字 @interface,定义一个新的annotation类型与定义一个接口非常像,自定义注解后就可以在任何地方使用了。后面细说。
定义2个请求接口
@TestLog("请求测试日志") @RequestMapping("/testOne") public String testOne(){ System.out.println("测试自定义注解"); return "testOne接口请正常"; } @TestLog("请求测试日志") @RequestMapping("testTwo") public String testTwo(){ System.out.println("测试自定义注解接口testTwo"); return "testTwo接口请正常"; }
现在自定义的注解已经都写在了testOne、testTwo接口上了,是不是就可以用了呢?
我们来请求testOne接口试试
http://127.0.0.1:8081/testOne
可以看到接口请求成功了,但是好像并没有实现注解什么功能
因此得知,这个注解目前没有任何作用,因为我们仅仅是对注解进行了声明,并没有在任何地方来使用这个注解,注解的本质也是一种广义的语法糖,最终还是要利用Java的反射来进行操作。
不过Java给我们提供了一个AOP机制,可以对类或方法进行动态的扩展,想较深入地了解这一机制的可以看一下这一篇文章: Spring AOP的实现原理及应用场景
创建切面类
/** * @PackageName: com.lagou.edu.aop * @author: youjp * @create: 2021-04-06 18:05 * @description: * @Version: 1.0 */ @Aspect @Component public class TestAspact { /** * 切点:连接的地方。这里与TestLog注解相关连 */ @Pointcut("@annotation(com.jp.demo.annotation.TestLog)") public void pointcut(){} /** * 拦截方法执行前。绑定切点。注意:annotation(log)和传参TestLog log相对应 * @param log */ @Before("pointcut()&& @annotation(log)") public void Before(TestLog log) throws Exception { System.out.println("--- 日志的内容为[" + log.value() + "] ---"); } }
其中pointcut声明了我们自定义的注解TestLog 。@Before代表在请求前通知,在具体的通知中通过@annotation(log)拿到了自定义的注解对象,所以就能够获取我们在使用注解时赋予的值了。
再次请求http://127.0.0.1:8081/testOne测试,可看到注解生效
使用注解获取更多详细信息
分别请求http://127.0.0.1:8081/testOne
测试,
分别请求http://127.0.0.1:8081/testTwo
测试.
可以看到打印的日志值相同的情况下,并不能知道是请求哪个接口输出的日志。现在我们来修改一下TestAspact的@Before通知方法
@Before("pointcut()&& @annotation(log)") public void Before(JoinPoint joinPoint,TestLog log) throws Exception { System.out.println("[" + joinPoint.getSignature().getDeclaringType().getSimpleName() + "][" + joinPoint.getSignature().getName() + "]-日志内容-[" + log.value() + "]"); }
通过JoinPoint可以获取到请求类、方法信息。现在可以清晰看到是哪个接口方法请求到的了。
JoinPoint常用方法API
使用注解获取请求参数
新增接口testThree
/** * 传参类接口 * @return */ @TestLog("请求testThree日志") @RequestMapping("testThree") public String testThree(String name,String age){ System.out.println("测试自定义注解接口testThree,获取传参:"+name); return "testThree接口请正常"; }
对TestAspact切面类修改
import com.jp.demo.annotation.TestLog; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; /** * @PackageName: com.lagou.edu.aop * @author: youjp * @create: 2021-04-06 18:05 * @description: * @Version: 1.0 */ @Aspect @Component public class TestAspact { /** * 切点:连接的地方。这里与TestLog注解相关连 */ @Pointcut("@annotation(com.jp.demo.annotation.TestLog)") public void pointcut(){} /** * 拦截方法执行前。绑定切点。注意:annotation(log)和传参TestLog log相对应 * @param log */ @Before("pointcut()&& @annotation(log)") public void Before(JoinPoint joinPoint,TestLog log) throws Exception { System.out.println("[" + joinPoint.getSignature().getDeclaringType().getSimpleName() + "][" + joinPoint.getSignature().getName() + "]-日志内容-[" + log.value() + "]"); } @Around("pointcut()&& @annotation(log)") public Object around(ProceedingJoinPoint joinPoint, TestLog log) throws Throwable { //获取传参字段信息 Map map= getFieldsName(joinPoint); Object args[]=joinPoint.getArgs(); System.out.println("[" + joinPoint.getSignature().getDeclaringType().getSimpleName() + "][" + joinPoint.getSignature().getName() + "]-请求传参" + map.entrySet()+"]"); return joinPoint.proceed(args); } /** * 获取字段值 * @param joinPoint * @return * @throws Exception */ private Map<String, Object> getFieldsName(JoinPoint joinPoint) throws Exception { String classType = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); // 参数值 Object[] args = joinPoint.getArgs(); Class<?>[] classes = new Class[args.length]; for (int k = 0; k < args.length; k++) { // 对于接受参数中含有MultipartFile,ServletRequest,ServletResponse类型的特殊处理,我这里是直接返回了null。(如果不对这三种类型判断,会报异常) if (args[k] instanceof MultipartFile || args[k] instanceof ServletRequest || args[k] instanceof ServletResponse) { return null; } if (!args[k].getClass().isPrimitive() && args[k]!=null) { // 当方法参数是基础类型,但是获取到的是封装类型的就需要转化成基础类型 // String result = args[k].getClass().getName(); // Class s = map.get(result); // 当方法参数是封装类型 Class s = args[k].getClass(); classes[k] = s == null ? args[k].getClass() : s; } } ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer(); // 获取指定的方法,第二个参数可以不传,但是为了防止有重载的现象,还是需要传入参数的类型 Method method = Class.forName(classType).getMethod(methodName, classes); // 参数名 String[] parameterNames = pnd.getParameterNames(method); // 通过map封装参数和参数值 HashMap<String, Object> paramMap = new HashMap(); for (int i = 0; i < parameterNames.length; i++) { paramMap.put(parameterNames[i], args[i]); } return paramMap; } }
请求http://localhost:8081/testThree?name=jp&age=12
如下
这里我们已经简单实现了自定义注解的常用功能。接下来,就针对案例进行讲解。
注解详细讲解
定义方式
注解其实就是一种标记,可以用来修饰,类、方法、变量、参数、包,但是它本身并不起任何作用,注解的作用在于注解的处理程序,通过捕获被注解标记的代码然后进行一些处理,这就是注解工作的方式。
在java中,自定义一个注解非常简单,通过@interface就能定义一个注解,实现如下:
public @interface TestLog{ }
根据我们在自定义类的经验,在类的实现部分无非就是书写构造、属性或方法。但是,在自定义注解中,其实现部分只能定义一个东西:注解类型元素(annotation type element)。咱们来看看其语法:
我们在定义属性的时候,如果只有一个元素可以默认写value
public @interface TestLog { String value(); }
这样在使用注解的时候直接写注解类(值)即可。也可以填写多个属性值
public @interface TestLog { public String name(); int age() default 18; int[] array(); }
定义注解类型元素时需要注意如下几点:
访问修饰符必须为public,不写默认为public;
该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型(体现了注解的嵌套效果)以及上述类型的一位数组;
该元素的名称一般定义为名词,如果注解中只有一个元素,请把名字起为value(后面使用会带来便利操作);
()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法; default代表默认值,值必须和第2点定义的类型一致;
如果没有默认值,代表后续使用注解时必须给该类型元素赋值。
元注解
元注解:对注解进行注解,也就是对注解进行标记,元注解的背后处理逻辑由apt tool提供,对注解的行为做出一些限制,例如生命周期,作用范围等等。
前面定义自定义注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface TestLog { String value() ; }
@ Target
@target注解用于描述作用的对象类型
public enum ElementType { /** 类,接口(包括注解类型)或枚举的声明 */ TYPE, /** 属性的声明 */ FIELD, /** 方法的声明 */ METHOD, /** 方法形式参数声明 */ PARAMETER, /** 构造方法的声明 */ CONSTRUCTOR, /** 局部变量声明 */ LOCAL_VARIABLE, /** 注解类型声明 */ ANNOTATION_TYPE, /** 包的声明 */ PACKAGE }