有些人总说自己过的不好,一上秤却又胖了不少
“我可以吃一口吗,就一小口~”
前言
在工作中经常发现,我们经常会使用一些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
}
@Retention注解
用于描述注解的生命周期,表示注解在什么范围有效,它有3个取值:
- Java源文件阶段;
- 编译到class文件阶段;
- 运行期阶段。
同样使用了RetentionPolicy枚举类型定义了三个阶段:
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
* (注解将被编译器忽略掉,常见的@Override就属于这种注解)
*/
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文件中,但在运行时不会被虚拟机保留,这是一个默认的行为。@Deprecated和@NonNull就属于这样的注解)
*/
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文件中,而且在运行时会被虚拟机保留,因此它们能通过反射被读取到;@Controller、@Service等都属于这一类)
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
@Documented
将注解的元素加入Javadoc中
@Inherited
是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解。@Inherited注解只对那些@Target被定义为ElementType.TYPE的自定义注解起作用。
@Repeatable
表示该注解可以重复标记
注解的特殊语法
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();
}
使用
//等效于@FirstAnnotation()
@FirstAnnotation
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{
//省略实现部分
}
4.如果一个注解的@Target是定义为Element.PACKAGE,那么这个注解是配置在package-info.java中的,而不能直接在某个类的package代码上面配置
自定义注解的运行时解析
只有当注解的保持力处于运行阶段,即使用@Retention(RetentionPolicy.RUNTIME)修饰注解时,才能在JVM运行时,检测到注解,并进行一系列特殊操作。
自定义的注解@CherryAnnotation,并把它配置在了类Student上
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface CherryAnnotation {
String name();
int age() default 23;
int[] score();
}
Student
public class Student {
@CherryAnnotation(name = "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!");
}
}
}
反射操作获取注解
public class Test {
public static void main(String[] args){
try {
//获取Student的Class对象
Class stuClass = Class.forName("cn.jp.pojo.Student");
//说明一下,这里形参不能写成Integer.class,应写为int.class
Method stuMethod = stuClass.getMethod("study",int.class);
//判断该元素上是否配置有CherryAnnotation注解;
if(stuMethod.isAnnotationPresent(CherryAnnotation.class)){
System.out.println("Student类上配置了CherryAnnotation注解!");
//获取该元素上指定类型的注解
CherryAnnotation cherryAnnotation = stuMethod.getAnnotation(CherryAnnotation.class);
System.out.println("name: " + cherryAnnotation.name() + ", age: " + cherryAnnotation.age()
+ ", score: " + cherryAnnotation.score()[0]);
}else{
System.out.println("Student类上没有配置CherryAnnotation注解!");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
如果我们要获得的注解是配置在方法上的,那么我们要从Method对象上获取;如果是配置在属性上,就需要从该属性对应的Field对象上去获取,如果是配置在类型上,需要从Class对象上去获取。总之在谁身上,就从谁身上去获取!
反射对象上还有一个方法getAnnotations(),该方法可以获得该对象身上配置的所有的注解。它会返回给我们一个注解数组,需要注意的是该数组的类型是Annotation类型,这个Annotation是一个来自于java.lang.annotation包的接口。
注解的核心原理
按照注解的生命周期以及处理方式的不同,通常将注解分为运行时注解和编译时注解
- 运行时注解的本质是实现了Annotation接口的特殊接口,JDK在运行时为其创建代理类,注解方法的调用实际是通过AnnotationInvocationHandler的invoke方法,AnnotationInvocationHandler其中维护了一个Map,Map中存放的是方法名与返回值的映射,对注解中自定义方法的调用其实最后就是用方法名去查Map并且放回的一个过程
- 编译时注解通过注解处理器来支持,而注解处理器的实际工作过程由JDK在编译期提供支持,有兴趣可以看看javac的源码。
注解的内容就大致介绍到这吧~~~~~~~~
有兴趣的老爷,可以关注我的公众号【一起收破烂】,回复【006】获取2021最新java面试资料以及简历模型120套哦~