Java 注解机制
注解是 JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它主要的作用有以下四方面:
【1】生成文档:通过代码里标识的元数据生成 javadoc文档。
【2】编译检查:通过代码里标识的元数据让编译器在编译期间进行检查验证。
【3】编译时动态处理:编译时通过代码里标识的元数据动态处理,例如动态生成代码。
【4】运行时动态处理:运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。
这么来说是比较抽象的,我们具体看下注解的常见分类:
【1】Java自带的标准注解:包括@Override、@Deprecated 和 @SuppressWarnings,分别用于表示重写某个方法、某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查。
【2】元注解:元注解是用于定义注解的注解,包括 @Retention用于标明注解被保留的阶段,@Target用于标明注解使用的范围,@Inherited用于标明注解可继承,@Documented用于标明是否生成 javadoc文档。
【3】自定义注解,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。
Java 内置注解
我们从最为常见的 Java内置的注解开始说起,先看下下面的代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class A{ 2 public void test() { 3 4 } 5 } 6 7 class B extends A{ 8 9 /** 10 * 重载父类的test方法 11 */ 12 @Override 13 public void test() { 14 } 15 16 /** 17 * 被弃用的方法 18 */ 19 @Deprecated 20 public void oldMethod() { 21 } 22 23 /** 24 * 忽略告警 25 * 26 * @return 27 */ 28 @SuppressWarnings("rawtypes") 29 public List processList() { 30 List list = new ArrayList(); 31 return list; 32 } 33 }
Java 1.5开始自带的标准注解,包括@Override、@Deprecated 和 @SuppressWarnings 同时通过这几个内置注解中的元注解的定义来引出元注解。
内置注解 @Override:我们先来看一下这个注解类型的定义:如下,从定义可以看出,这个注解可以被用来修饰方法,并且它只在编译时有效,在编译后的 class文件中便不再存在。这个注解的作用我们大家都不陌生,那就是告诉编译器被修饰的方法是重写的父类的中的相同签名的方法,编译器会对此做出检查,若发现父类中不存在这个方法或是存在的方法签名不同,则会报错。
1 @Target(ElementType.METHOD) 2 @Retention(RetentionPolicy.SOURCE) 3 public @interface Override { 4 }
内置注解 @Deprecated:定义如下,从它的定义我们可以知道,它会被文档化,能够保留到运行时,能够修饰构造方法、属性、局部变量、方法、包、参数、类型。这个注解的作用是告诉编译器被修饰的程序元素已被“废弃”,不再建议用户使用。
1 @Documented 2 @Retention(RetentionPolicy.RUNTIME) 3 @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) 4 public @interface Deprecated { 5 }
内置注解 @SuppressWarnings:这个注解我们也比较常用到,先来看下它的定义:
1 @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) 2 @Retention(RetentionPolicy.SOURCE) 3 public @interface SuppressWarnings { 4 String[] value(); 5 }
它能够修饰的程序元素包括类型、属性、方法、参数、构造器、局部变量,只能存活在源码时,取值为String[]。它的作用是告诉编译器忽略指定的警告信息,它可以取的值如下所示:
参数 | 作用 | 原描述 |
---|---|---|
all | 抑制所有警告 | to suppress all warnings |
boxing | 抑制装箱、拆箱操作时候的警告 | to suppress warnings relative to boxing/unboxing operations |
cast | 抑制映射相关的警告 | to suppress warnings relative to cast operations |
dep-ann | 抑制启用注释的警告 | to suppress warnings relative to deprecated annotation |
deprecation | 抑制过期方法警告 | to suppress warnings relative to deprecation |
fallthrough | 抑制确在switch中缺失breaks的警告 | to suppress warnings relative to missing breaks in switch statements |
finally | 抑制finally模块没有返回的警告 | to suppress warnings relative to finally block that don’t return |
hiding | 抑制与隐藏变数的区域变数相关的警告 | to suppress warnings relative to locals that hide variable() |
incomplete-switch | 忽略没有完整的switch语句 | to suppress warnings relative to missing entries in a switch statement (enum case) |
nls | 忽略非nls格式的字符 | to suppress warnings relative to non-nls string literals |
null | 忽略对null的操作 | to suppress warnings relative to null analysis |
rawtype | 使用generics时忽略没有指定相应的类型 | to suppress warnings relative to un-specific types when using |
restriction | 抑制与使用不建议或禁止参照相关的警告 | to suppress warnings relative to usage of discouraged or |
serial | 忽略在serializable类中没有声明serialVersionUID变量 | to suppress warnings relative to missing serialVersionUID field for a serializable class |
static-access | 抑制不正确的静态访问方式警告 | to suppress warnings relative to incorrect static access |
synthetic-access | 抑制子类没有按最优方法访问内部类的警告 | to suppress warnings relative to unoptimized access from inner classes |
unchecked | 抑制没有进行类型检查操作的警告 | to suppress warnings relative to unchecked operations |
unqualified-field-access | 抑制没有权限访问的域的警告 | to suppress warnings relative to field access unqualified |
unused | 抑制没被使用过的代码的警告 | to suppress warnings relative to unused code |
元注解
上述内置注解的定义中使用了一些元注解(注解类型进行注解的注解类),在JDK 1.5中提供了4个标准的元注解:@Target、@Retention、@Documented、@Inherited,在JDK 1.8中提供了两个元注解 @Repeatable 和 @Native。
@Target 元注解:描述注解的使用范围(即:被修饰的注解可以用在什么地方)注解可以用于修饰 packages、types(类、接口、枚举、注解类)、类成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数),在定义注解类时使用了 @Target 能够更加清晰的知道它能够被用来修饰哪些对象,它的取值范围定义在ElementType 枚举中。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public enum ElementType { 2 3 TYPE, // 类、接口、枚举类 4 5 FIELD, // 成员变量(包括:枚举常量) 6 7 METHOD, // 成员方法 8 9 PARAMETER, // 方法参数 10 11 CONSTRUCTOR, // 构造方法 12 13 LOCAL_VARIABLE, // 局部变量 14 15 ANNOTATION_TYPE, // 注解类 16 17 PACKAGE, // 可用于修饰:包 18 19 TYPE_PARAMETER, // 类型参数,JDK 1.8 新增 20 21 TYPE_USE // 使用类型的任何地方,JDK 1.8 新增 22 }
@Retention & @RetentionTarget 元注解:描述注解保留的时间范围(即:被描述的注解在它所修饰的类中可以被保留到何时)Reteniton 注解用来限定那些被它所注解的注解类在注解到其他类上以后,可被保留到何时,一共有三种策略,定义在RetentionPolicy 枚举中。
1 public enum RetentionPolicy { 2 SOURCE, // 源文件保留 3 CLASS, // 编译期保留,默认值 4 RUNTIME // 运行期保留,可通过反射去获取注解信息 5 }
为了验证应用了这三种策略的注解类有何区别,分别使用三种策略各定义一个注解类做测试。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 @Retention(RetentionPolicy.SOURCE) 2 public @interface SourcePolicy { 3 4 } 5 @Retention(RetentionPolicy.CLASS) 6 public @interface ClassPolicy { 7 8 } 9 @Retention(RetentionPolicy.RUNTIME) 10 public @interface RuntimePolicy { 11 12 }
用定义好的三个注解类分别去注解一个方法。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class RetentionTest { 2 3 @SourcePolicy 4 public void sourcePolicy() { 5 } 6 7 @ClassPolicy 8 public void classPolicy() { 9 } 10 11 @RuntimePolicy 12 public void runtimePolicy() { 13 } 14 }
通过执行 javap -verbose RetentionTest 命令获取到的 RetentionTest 的 class 字节码内容如下。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 { 2 public retention.RetentionTest(); 3 flags: ACC_PUBLIC 4 Code: 5 stack=1, locals=1, args_size=1 6 0: aload_0 7 1: invokespecial #1 // Method java/lang/Object."<init>":()V 8 4: return 9 LineNumberTable: 10 line 3: 0 11 12 public void sourcePolicy(); 13 flags: ACC_PUBLIC 14 Code: 15 stack=0, locals=1, args_size=1 16 0: return 17 LineNumberTable: 18 line 7: 0 19 20 public void classPolicy(); 21 flags: ACC_PUBLIC 22 Code: 23 stack=0, locals=1, args_size=1 24 0: return 25 LineNumberTable: 26 line 11: 0 27 RuntimeInvisibleAnnotations: 28 0: #11() 29 30 public void runtimePolicy(); 31 flags: ACC_PUBLIC 32 Code: 33 stack=0, locals=1, args_size=1 34 0: return 35 LineNumberTable: 36 line 15: 0 37 RuntimeVisibleAnnotations: 38 0: #14() 39 }
从 RetentionTest 的字节码内容我们可以得出以下两点结论:
【1】编译器并没有记录下 sourcePolicy() 方法的注解信息;
【2】编译器分别使用了 RuntimeInvisibleAnnotations 和 RuntimeVisibleAnnotations 属性去记录了 classPolicy()方法和runtimePolicy()方法的注解信息;
@Documented 元注解:描述在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息。以下代码在使用 Javadoc工具可以生成 @TestDocAnnotation注解信息。
1 import java.lang.annotation.Documented; 2 import java.lang.annotation.ElementType; 3 import java.lang.annotation.Target; 4 5 @Documented 6 @Target({ElementType.TYPE,ElementType.METHOD}) 7 public @interface TestDocAnnotation { 8 public String value() default "default"; 9 }
用户便可以在方法上添加自定义的注解
1 @TestDocAnnotation("myMethodDoc") 2 public void testDoc() { 3 4 }
@Inherited 元注解:被它修饰的 Annotation将具有继承性。如果某个类使用了被 @Inherited修饰的Annotation,则其子类将自动具有该注解。
1 @Inherited 2 @Retention(RetentionPolicy.RUNTIME) 3 @Target({ElementType.TYPE,ElementType.METHOD}) 4 public @interface TestInheritedAnnotation { 5 String [] values(); 6 int number(); 7 }
使用这个注解:
1 @TestInheritedAnnotation(values = {"value"}, number = 10) 2 public class Person { 3 } 4 5 class Student extends Person{ 6 @Test 7 public void test(){ 8 Class clazz = Student.class; 9 Annotation[] annotations = clazz.getAnnotations(); 10 for (Annotation annotation : annotations) { 11 System.out.println(annotation.toString()); 12 } 13 } 14 }
输出:即使 Student类没有显示地被注解 @TestInheritedAnnotation,但是它的父类 Person被注解,而且@TestInheritedAnnotation 被 @Inherited注解,因此 Student类自动有了该注解。
xxxxxxx.TestInheritedAnnotation(values=[value], number=10)
@Repeatable (Java8)元注解:允许在同一申明类型(类,属性,或方法)的多次使用同一个注解。不同的地方是,创建重复注解Authority时,加上@Repeatable,指向存储注解Authorities,在使用时候,直接可以重复使用Authority注解。从上面例子看出,java 8里面做法更适合常规的思维,可读性强一点。
1 @Repeatable(Authorities.class) 2 public @interface Authority { 3 String role(); 4 } 5 6 public @interface Authorities { 7 Authority[] value(); 8 } 9 10 public class RepeatAnnotationUseNewVersion { 11 @Authority(role="Admin") 12 @Authority(role="Manager") 13 public void doSomeThing(){ } 14 }
@Native (Java8)元注解:使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可
注解与反射接口
定义注解后,如何获取注解中的内容呢?反射包 java.lang.reflect下的 AnnotatedElement接口提供这些方法。这里注意:只有注解被定义为 RUNTIME后,该注解才能是运行时可见,当 class文件被装载时被保存在 class文件中的 Annotation才会被虚拟机读取。
AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement 对象之后,程序就可以调用该对象的方法来访问 Annotation信息。我们看下具体的接口:
【1】判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false。注意:此方法会忽略注解对应的注解容器。
boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)
【2】返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。
<T extends Annotation> T getAnnotation(Class<T> annotationClass)
【3】 返回该程序元素上存在的所有注解,若没有注解,返回长度为0的数组。
Annotation[] getAnnotations()
【4】 返回该程序元素上存在的、指定类型的注解数组。没有注解对应类型的注解时,返回长度为0的数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。getAnnotationsByType方法与 getAnnotation的区别在于,getAnnotationsByType会检测注解对应的重复注解容器。若程序元素为类,当前类上找不到注解,且该注解为可继承的,则会去父类上检测对应的注解。
<T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass)
【5】返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释。如果没有注释直接存在于此元素上,则返回null
<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass)
【6】返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释
<T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass)
【7】返回直接存在于此元素上的所有注解及注解对应的重复注解容器。与此接口中的其他方法不同,该方法将忽略继承的注解。如果没有注释直接存在于此元素上,则返回长度为零的一个数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。
Annotation[] getDeclaredAnnotations()
自定义注解
当我们理解了内置注解,元注解和获取注解的反射接口后,我们便可以开始自定义注解了。这个例子我把上述的知识点全部融入进来,代码很简单:
1 package com.pdai.java.annotation; 2 3 import java.lang.annotation.ElementType; 4 import java.lang.annotation.Retention; 5 import java.lang.annotation.RetentionPolicy; 6 import java.lang.annotation.Target; 7 8 @Target(ElementType.METHOD) 9 @Retention(RetentionPolicy.RUNTIME) 10 public @interface MyMethodAnnotation { 11 12 public String title() default ""; 13 14 public String description() default ""; 15 16 }
使用注解
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.pdai.java.annotation; 2 3 import java.io.FileNotFoundException; 4 import java.lang.annotation.Annotation; 5 import java.lang.reflect.Method; 6 import java.util.ArrayList; 7 import java.util.List; 8 9 public class TestMethodAnnotation { 10 11 @Override 12 @MyMethodAnnotation(title = "toStringMethod", description = "override toString method") 13 public String toString() { 14 return "Override toString method"; 15 } 16 17 @Deprecated 18 @MyMethodAnnotation(title = "old static method", description = "deprecated old static method") 19 public static void oldMethod() { 20 System.out.println("old method, don't use it."); 21 } 22 23 @SuppressWarnings({"unchecked", "deprecation"}) 24 @MyMethodAnnotation(title = "test method", description = "suppress warning static method") 25 public static void genericsTest() throws FileNotFoundException { 26 List l = new ArrayList(); 27 l.add("abc"); 28 oldMethod(); 29 } 30 }
用反射接口获取注解信息:在 TestMethodAnnotation中添加 Main方法进行测试:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public static void main(String[] args) { 2 try { 3 // 获取所有methods 4 Method[] methods = TestMethodAnnotation.class.getClassLoader() 5 .loadClass(("com.pdai.java.annotation.TestMethodAnnotation")) 6 .getMethods(); 7 8 // 遍历 9 for (Method method : methods) { 10 // 方法上是否有MyMethodAnnotation注解 11 if (method.isAnnotationPresent(MyMethodAnnotation.class)) { 12 try { 13 // 获取并遍历方法上的所有注解 14 for (Annotation anno : method.getDeclaredAnnotations()) { 15 System.out.println("Annotation in Method '" 16 + method + "' : " + anno); 17 } 18 19 // 获取MyMethodAnnotation对象信息 20 MyMethodAnnotation methodAnno = method 21 .getAnnotation(MyMethodAnnotation.class); 22 23 System.out.println(methodAnno.title()); 24 25 } catch (Throwable ex) { 26 ex.printStackTrace(); 27 } 28 } 29 } 30 } catch (SecurityException | ClassNotFoundException e) { 31 e.printStackTrace(); 32 } 33 }
测试的输出:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 Annotation in Method 'public static void com.pdai.java.annotation.TestMethodAnnotation.oldMethod()' : @java.lang.Deprecated() 2 Annotation in Method 'public static void com.pdai.java.annotation.TestMethodAnnotation.oldMethod()' : @com.pdai.java.annotation.MyMethodAnnotation(title=old static method, description=deprecated old static method) 3 old static method 4 Annotation in Method 'public static void com.pdai.java.annotation.TestMethodAnnotation.genericsTest() throws java.io.FileNotFoundException' : @com.pdai.java.annotation.MyMethodAnnotation(title=test method, description=suppress warning static method) 5 test method 6 Annotation in Method 'public java.lang.String com.pdai.java.annotation.TestMethodAnnotation.toString()' : @com.pdai.java.annotation.MyMethodAnnotation(title=toStringMethod, description=override toString method) 7 toStringMethod
深入理解注解
ElementType.TYPE_PARAMETER:ElementType.TYPE_USE(此类型包括类型声明和类型参数声明,是为了方便设计者进行类型检查)包含了ElementType.TYPE(类、接口(包括注解类型)和枚举的声明)和 ElementType.TYPE_PARAMETER(类型参数声明),不妨再看个例子
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // 自定义ElementType.TYPE_PARAMETER注解 2 @Retention(RetentionPolicy.RUNTIME) 3 @Target(ElementType.TYPE_PARAMETER) 4 public @interface MyNotEmpty { 5 } 6 7 // 自定义ElementType.TYPE_USE注解 8 @Retention(RetentionPolicy.RUNTIME) 9 @Target(ElementType.TYPE_USE) 10 public @interface MyNotNull { 11 } 12 13 // 测试类 14 public class TypeParameterAndTypeUseAnnotation<@MyNotEmpty T>{ 15 16 //使用TYPE_PARAMETER类型,会编译不通过 17 // public @MyNotEmpty T test(@MyNotEmpty T a){ 18 // new ArrayList<@MyNotEmpty String>(); 19 // return a; 20 // } 21 22 //使用TYPE_USE类型,编译通过 23 public @MyNotNull T test2(@MyNotNull T a){ 24 new ArrayList<@MyNotNull String>(); 25 return a; 26 } 27 }
注解支持继承吗?
注解是不支持继承的。不能使用关键字 extends来继承某个 @interface,但注解在编译后,编译器会自动继承java.lang.annotation.Annotation接口。虽然反编译后发现注解继承了 Annotation接口,请记住,即使 Java的接口可以实现多继承,但定义注解时依然无法使用 extends关键字继承 @interface。
区别于注解的继承,被注解的子类继承父类注解可以用@Inherited: 如果某个类使用了被 @Inherited修饰的 Annotation,则其子类将自动具有该注解。
注解实现的原理?
这里推荐你两篇文章:
- https://blog.csdn.net/qq_20009015/article/details/106038023
- https://www.race604.com/annotation-processing/
注解的应用场景
【1】配置化到注解化 - 框架的演进:Spring 框架配置化到注解化的转变。
【2】继承实现到注解实现 - Junit3 到 Junit4:一个模块的封装大多数人都是通过继承和组合等模式来实现的,但是如果结合注解将可以极大程度提高实现的优雅度(降低耦合度)。而 Junit3 到 Junit4的演化就是最好的一个例子。
【被测试类】
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class HelloWorld { 2 3 public void sayHello(){ 4 System.out.println("hello...."); 5 throw new NumberFormatException(); 6 } 7 8 public void sayWorld(){ 9 System.out.println("world...."); 10 } 11 12 public String say(){ 13 return "hello world!"; 14 } 15 16 }
【Junit 3 实现UT】:通过继承 TestCase来实现,初始化是通过 Override父类方法来进行,测试方式通过 test的前缀方法获取。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class HelloWorldTest extends TestCase{ 2 private HelloWorld hw; 3 4 @Override 5 protected void setUp() throws Exception { 6 super.setUp(); 7 hw=new HelloWorld(); 8 } 9 10 //1.测试没有返回值 11 public void testHello(){ 12 try { 13 hw.sayHello(); 14 } catch (Exception e) { 15 System.out.println("发生异常....."); 16 } 17 18 } 19 public void testWorld(){ 20 hw.sayWorld(); 21 } 22 //2.测试有返回值的方法 23 // 返回字符串 24 public void testSay(){ 25 assertEquals("测试失败", hw.say(), "hello world!"); 26 } 27 //返回对象 28 public void testObj(){ 29 assertNull("测试对象不为空", null); 30 assertNotNull("测试对象为空",new String()); 31 } 32 @Override 33 protected void tearDown() throws Exception { 34 super.tearDown(); 35 hw=null; 36 } 37 }
【Junit 4 实现UT】:通过定义@Before,@Test,@After等等注解来实现。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class HelloWorldTest { 2 private HelloWorld hw; 3 4 @Before 5 public void setUp() { 6 hw = new HelloWorld(); 7 } 8 9 @Test(expected=NumberFormatException.class) 10 // 1.测试没有返回值,有别于junit3的使用,更加方便 11 public void testHello() { 12 hw.sayHello(); 13 } 14 @Test 15 public void testWorld() { 16 hw.sayWorld(); 17 } 18 19 @Test 20 // 2.测试有返回值的方法 21 // 返回字符串 22 public void testSay() { 23 assertEquals("测试失败", hw.say(), "hello world!"); 24 } 25 26 @Test 27 // 返回对象 28 public void testObj() { 29 assertNull("测试对象不为空", null); 30 assertNotNull("测试对象为空", new String()); 31 } 32 33 @After 34 public void tearDown() throws Exception { 35 hw = null; 36 } 37 38 }
这里我们发现通过注解的方式,我们实现单元测试时将更为优雅。如果你还期望了解Junit4是如何实现运行的呢?可以看这篇文章:JUnit4源码分析运行原理
自定义注解和AOP - 通过切面实现解耦
最为常见的就是使用Spring AOP切面实现统一的操作日志管理,我这里找了一个开源项目中的例子(只展示主要代码),给你展示下如何通过注解实现解耦的。
【1】自定义 Log注解
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 @Target({ ElementType.PARAMETER, ElementType.METHOD }) 2 @Retention(RetentionPolicy.RUNTIME) 3 @Documented 4 public @interface Log { 5 /** 6 * 模块 7 */ 8 public String title() default ""; 9 10 /** 11 * 功能 12 */ 13 public BusinessType businessType() default BusinessType.OTHER; 14 15 /** 16 * 操作人类别 17 */ 18 public OperatorType operatorType() default OperatorType.MANAGE; 19 20 /** 21 * 是否保存请求的参数 22 */ 23 public boolean isSaveRequestData() default true; 24 }
【2】实现日志的切面, 对自定义注解 Log作切点进行拦截:即对注解 @Log的方法进行切点拦截
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 @Aspect 2 @Component 3 public class LogAspect { 4 private static final Logger log = LoggerFactory.getLogger(LogAspect.class); 5 6 /** 7 * 配置织入点 - 自定义注解的包路径 8 * 9 */ 10 @Pointcut("@annotation(com.xxx.aspectj.lang.annotation.Log)") 11 public void logPointCut() { 12 } 13 14 /** 15 * 处理完请求后执行 16 * 17 * @param joinPoint 切点 18 */ 19 @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult") 20 public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) { 21 handleLog(joinPoint, null, jsonResult); 22 } 23 24 /** 25 * 拦截异常操作 26 * 27 * @param joinPoint 切点 28 * @param e 异常 29 */ 30 @AfterThrowing(value = "logPointCut()", throwing = "e") 31 public void doAfterThrowing(JoinPoint joinPoint, Exception e) { 32 handleLog(joinPoint, e, null); 33 } 34 35 protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) { 36 try { 37 // 获得注解 38 Log controllerLog = getAnnotationLog(joinPoint); 39 if (controllerLog == null) { 40 return; 41 } 42 43 // 获取当前的用户 44 User currentUser = ShiroUtils.getSysUser(); 45 46 // *========数据库日志=========*// 47 OperLog operLog = new OperLog(); 48 operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); 49 // 请求的地址 50 String ip = ShiroUtils.getIp(); 51 operLog.setOperIp(ip); 52 // 返回参数 53 operLog.setJsonResult(JSONObject.toJSONString(jsonResult)); 54 55 operLog.setOperUrl(ServletUtils.getRequest().getRequestURI()); 56 if (currentUser != null) { 57 operLog.setOperName(currentUser.getLoginName()); 58 if (StringUtils.isNotNull(currentUser.getDept()) 59 && StringUtils.isNotEmpty(currentUser.getDept().getDeptName())) { 60 operLog.setDeptName(currentUser.getDept().getDeptName()); 61 } 62 } 63 64 if (e != null) { 65 operLog.setStatus(BusinessStatus.FAIL.ordinal()); 66 operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); 67 } 68 // 设置方法名称 69 String className = joinPoint.getTarget().getClass().getName(); 70 String methodName = joinPoint.getSignature().getName(); 71 operLog.setMethod(className + "." + methodName + "()"); 72 // 设置请求方式 73 operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); 74 // 处理设置注解上的参数 75 getControllerMethodDescription(controllerLog, operLog); 76 // 保存数据库 77 AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); 78 } catch (Exception exp) { 79 // 记录本地异常日志 80 log.error("==前置通知异常=="); 81 log.error("异常信息:{}", exp.getMessage()); 82 exp.printStackTrace(); 83 } 84 } 85 86 /** 87 * 获取注解中对方法的描述信息 用于Controller层注解 88 * 89 * @param log 日志 90 * @param operLog 操作日志 91 * @throws Exception 92 */ 93 public void getControllerMethodDescription(Log log, OperLog operLog) throws Exception { 94 // 设置action动作 95 operLog.setBusinessType(log.businessType().ordinal()); 96 // 设置标题 97 operLog.setTitle(log.title()); 98 // 设置操作人类别 99 operLog.setOperatorType(log.operatorType().ordinal()); 100 // 是否需要保存request,参数和值 101 if (log.isSaveRequestData()) { 102 // 获取参数的信息,传入到数据库中。 103 setRequestValue(operLog); 104 } 105 } 106 107 /** 108 * 获取请求的参数,放到log中 109 * 110 * @param operLog 111 * @param request 112 */ 113 private void setRequestValue(OperLog operLog) { 114 Map<String, String[]> map = ServletUtils.getRequest().getParameterMap(); 115 String params = JSONObject.toJSONString(map); 116 operLog.setOperParam(StringUtils.substring(params, 0, 2000)); 117 } 118 119 /** 120 * 是否存在注解,如果存在就获取 121 */ 122 private Log getAnnotationLog(JoinPoint joinPoint) throws Exception { 123 Signature signature = joinPoint.getSignature(); 124 MethodSignature methodSignature = (MethodSignature) signature; 125 Method method = methodSignature.getMethod(); 126 127 if (method != null) 128 { 129 return method.getAnnotation(Log.class); 130 } 131 return null; 132 } 133 }
【3】使用 @Log注解:以一个简单的 CRUD操作为例,这里展示部分代码:每对“部门”进行操作就会产生一条操作日志存入数据库
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 @Controller 2 @RequestMapping("/system/dept") 3 public class DeptController extends BaseController { 4 private String prefix = "system/dept"; 5 6 @Autowired 7 private IDeptService deptService; 8 9 /** 10 * 新增保存部门 11 */ 12 @Log(title = "部门管理", businessType = BusinessType.INSERT) 13 @RequiresPermissions("system:dept:add") 14 @PostMapping("/add") 15 @ResponseBody 16 public AjaxResult addSave(@Validated Dept dept) { 17 if (UserConstants.DEPT_NAME_NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) { 18 return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在"); 19 } 20 return toAjax(deptService.insertDept(dept)); 21 } 22 23 /** 24 * 保存 25 */ 26 @Log(title = "部门管理", businessType = BusinessType.UPDATE) 27 @RequiresPermissions("system:dept:edit") 28 @PostMapping("/edit") 29 @ResponseBody 30 public AjaxResult editSave(@Validated Dept dept) { 31 if (UserConstants.DEPT_NAME_NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) { 32 return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在"); 33 } else if(dept.getParentId().equals(dept.getDeptId())) { 34 return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己"); 35 } 36 return toAjax(deptService.updateDept(dept)); 37 } 38 39 /** 40 * 删除 41 */ 42 @Log(title = "部门管理", businessType = BusinessType.DELETE) 43 @RequiresPermissions("system:dept:remove") 44 @GetMapping("/remove/{deptId}") 45 @ResponseBody 46 public AjaxResult remove(@PathVariable("deptId") Long deptId) { 47 if (deptService.selectDeptCount(deptId) > 0) { 48 return AjaxResult.warn("存在下级部门,不允许删除"); 49 } 50 if (deptService.checkDeptExistUser(deptId)) { 51 return AjaxResult.warn("部门存在用户,不允许删除"); 52 } 53 return toAjax(deptService.deleteDeptById(deptId)); 54 } 55 56 // ... 57 }
同样的,你也可以看到权限管理也是通过类似的注解(@RequiresPermissions
)机制来实现的。所以我们可以看到,通过注解+AOP最终的目标是为了实现模块的解耦。