java注解详细介绍(java菜鸟到大佬全网最全注解机制讲解)
java注解详细介绍(java菜鸟到大佬全网最全注解机制讲解)· 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。· 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。注解应用的三个真实业务?注解是jdk1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它主要的作用有以下四方面:· 生成文档,通过代码里标识的元数据生成javadoc文档。
目录:java注解基础知识?
Java内置注解?
元注解?
自定义注解?
注解应用的三个真实业务?
Java注解基础知识注解是jdk1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它主要的作用有以下四方面:
· 生成文档,通过代码里标识的元数据生成javadoc文档。
· 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
· 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
· 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。
这么来说是比较抽象的,我们具体看下注解的常见分类:
· Java自带的标准注解,包括@Override、@Deprecated和@SuppressWarnings,分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查。
· 元注解,元注解是用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented,
@Retention用于标明注解被保留的阶段
@Target用来限制注解的使用范围
@Inherited该注解使父类的注解能被其子类继承
@Documented该注解是一个标记注解,用于指示一个注解将被文档化
@Repeatable 该注解是Java8新增的注解,用于开发重复注解
@类型注解(Type Annotation)该注解是Java8新增的注解,用于开发重复注解
· 自定义注解,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。
接下来我们通过这个分类角度来理解注解。
Java内置注解JDK 中内置了以下注解:
注解名称 |
功能描述 |
@Override |
检查改方法是否是重写方法,如果发现其父类或者是引用的接口中并没有该方法时,会报编译错误 |
@Deprecated |
标记过时方法,如果使用该方法会报编译警告 |
@SuppressWarnings |
指示编译器去忽略注释解中声明的警告 |
@FunctionalInterface |
Java8支持,标识一个匿名函数或函数式接口 |
内置注解 - @Override
概念:检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
//这个extends 不要在意,我写上去只是为了更加方便直观的去理解,Object是万物之源,不写也会默认是其子类,不用解释过多吧?
public class Annotation1 extends Object{
@Override
public String toString(){
return"我是重新定义过的toString方法";
}
}
@Override(重写),这个大家应该很熟悉,重写父类的方法。我们可以看下Object类中toString()是什么样子的。
那么显而易见,使用了@Override(重写)注解,方法名、方法参数必须得和父类保持一致,否则会报错。如下图所示:
如果不加@Override(重写)注解,则正常编译。
内置注解 - @Deprecated
概念:标记过时方法。如果使用该方法,会报编译警告。 在开发中,我们经常能遇到这样的情况,如下图:
在jdk中有大量这样的方法,我就不举例了,自己写一个可能会更加方便理解。
public class Annotation1 extends Object{
publicstaticvoidmain(String[] args){
}
@SuppressWarnings("all")
publicstaticvoidtestSuppressWarnings(){
System.out.println("测试 testSuppressWarnings忽略警告!");
}
}
注意点: 这个不是报错,只是警告,提醒我们这个方法可能会有问题,可能有更好的方法来实现!
内置注解 - @SuppressWarnings
概念:指示编译器去忽略注解中声明的警告。
平时开发中,我们会遇到这样的情况,如下图:
这也不是错误,这是提醒我们,该方法没有使用到,警告提醒的作用。加上@SuppressWarnings注解后。
public class Annotation1 extends Object {
publicstaticvoidmain(String[] args) {
}
@SuppressWarnings("all")
publicstaticvoidtestSuppressWarnings() {
System.out.println("测试 testSuppressWarnings忽略警告!");
}
}
方法成功高亮起来,并且没有警告提示了!
我们可以点进去看下这个注解为什么需要参数?
看这里,这个不是方法哦,这是参数。
在注解中的参数格式:calss 参数名 ()!这个需要强行记忆哦,回头我们自定义注解时也需要用到。换一种写法加深理解!如下图:
注意点:当注解中只有一个参数时,我们无需加上参数名,注解会自动帮我们匹配的。
@SuppressWarnings 有常见的值,分别对应如下意思
1.deprecation:使用了不赞成使用的类或方法时的警告(使用@Deprecated使得编译器产生的警告);
2.unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型; 关闭编译器警告
3.fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
4.path:在类路径、源文件路径等中有不存在的路径时的警告;
5.serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
6.finally:任何 finally 子句不能正常完成时的警告;
7.rawtypes 泛型类型未指明
8.unused 引用定义了,但是没有被使用
9.all:关于以上所有情况的警告。
package annotation;
import java.util.ArrayList;
import java.util.List;
public class Hero {
String name;
@SuppressWarnings({ "rawtypes" "unused" })
public static void main(String[] args) {
List heros = new ArrayList();
}
}
内置注解 - @FunctionalInterface
@FunctionalInterface这是Java1.8 新增的注解,用于约定函数式接口。
函数式接口概念: 如果接口中只有一个抽象方法(可以包含多个默认方法或多个static方法),该接口称为函数式接口。函数式接口其存在的意义,主要是配合Lambda 表达式 来使用。
如例所示,AD接口只有一个adAttack方法,那么就可以被注解为@FunctionalInterface 而AP接口有两个方法apAttack()和apAttack2() 那么就不能被注解为函数式接口
AD :
package annotation;
@FunctionalInterface
public interface AD {
public void adAttack();
}
AP:
package annotation;
@FunctionalInterface
public interface AP {
public void adAttack();
public void apAttack2();
}
元注解
概念:顾名思义,元注解就是给注解使用的注解!
@Retention 作用域-(常用)
概念:表示在什么级别保存该注解信息。 在实际开发中,我们一般都写RUNTIME,除非项目有特殊需求!我们看下@Retention的源码。
可以看到,需要一个参数,进参数瞅瞅。
SOURCE: 源代码时有用。
CLASS: class文件中有用,但会被jvm丢弃。
RUNTIME: 运行时有用。
关系:RUNTIME>CLASS>SOURCE后面我们自定义注解时,每个都需要用该注解!
@Documented 作用文档
概念:将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。老规矩看下源码:
无参的注解,作用域为RetentionPolicy.RUNTIME,运行时有用!这个只是用来作为标记,了解即可,在实际运行后会将该注解写入javadoc中,方便查看。
@Target 目标-(常用)
概念:标记这个注解应该是使用在哪种 Java 成员上面!
参数源码:
注意这里是数组格式的参数,证明可以传多个值。
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(
ElementType.LOCAL_VARIABLE)——局部变量
@Target(
ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包
我们来试一下:
目标不对会报错的哦!我们将其改成方法上!编译即正常通过。
其他的作用域大家可以去自行尝试,篇幅问题,无法做到每个都去试一遍!
@Inherited 继承
概念:标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)。
这个很简单,就是当@InheritedAnno注解加在某个类A上时,假如类B继承了A,则B也会带上该注解。
新注解-(了解即可)
从 Java 7 开始,额外添加了 3 个注解:
@SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
@FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
@Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
三、自定义注解当我们理解了内置注解 元注解和获取注解的反射接口后,我们便可以开始自定义注解了。这个例子我把上述的知识点全部融入进来 代码很简单:
- 定义自己的注解
package com.pdai.java.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMethodAnnotation {
public String title() default "";
public String description() default "";
}
- 使用注解
package com.pdai.java.annotation;
import java.io.FileNotFoundException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class TestMethodAnnotation {
@Override
@MyMethodAnnotation(title = "toStringMethod" description = "override toString method")
public String toString() {
return "Override toString method";
}
@Deprecated
@MyMethodAnnotation(title = "old static method" description = "deprecated old static method")
public static void oldMethod() {
System.out.println("old method don't use it.");
}
@SuppressWarnings({"unchecked" "deprecation"})
@MyMethodAnnotation(title = "test method" description = "suppress warning static method")
public static void genericsTest() throws FileNotFoundException {
List l = new ArrayList();
l.add("abc");
oldMethod();
}
}
- 用反射接口获取注解信息
在TestMethodAnnotation中添加Main方法进行测试:
public static void main(String[] args) {
try {
// 获取所有methods
Method[] methods = TestMethodAnnotation.class.getClassLoader()
.loadClass(("com.pdai.java.annotation.TestMethodAnnotation"))
.getMethods();
// 遍历
for (Method method : methods) {
// 方法上是否有MyMethodAnnotation注解
if (method.isAnnotationPresent(MyMethodAnnotation.class)) {
try {
// 获取并遍历方法上的所有注解
for (Annotation anno : method.getDeclaredAnnotations()) {
System.out.println("Annotation in Method '"
method "' : " anno);
}
// 获取MyMethodAnnotation对象信息
MyMethodAnnotation methodAnno = method
.getAnnotation(MyMethodAnnotation.class);
System.out.println(methodAnno.title());
} catch (Throwable ex) {
ex.printStackTrace();
}
}
}
} catch (SecurityException | ClassNotFoundException e) {
e.printStackTrace();
}
}
- 测试的输出
Annotation in Method 'public static void com.pdai.java.annotation.TestMethodAnnotation.oldMethod()' : @java.lang.Deprecated()
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)
old static method
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)
test method
Annotation in Method 'public java.lang.String com.pdai.java.annotation.TestMethodAnnotation.toString()' : @com.pdai.java.annotation.MyMethodAnnotation(title=toStringMethod description=override toString method)
toStringMethod
Java8提供了哪些新的注解?
重复注解(ElementType.TYPE_USE)
允许在同一声明类型(类,属性,或方法)上多次使用同一个注解。
Java8以前的版本使用注解有一个限制是相同的注解在同一位置只能使用一次,不能使用多次。
Java 8 引入了重复注解机制,这样相同的注解可以在同一地方使用多次。重复注解机制本身必须用 @Repeatable 注解。
实际上,重复注解不是一个语言上的改变,只是编译器层面的改动,技术层面仍然是一样的。
例如,我们可以使用如下示例来具体对比Java8之前的版本和Java8中的注解。
1)自定义一个包装类Hints注解用来放置一组具体的Hint注解
@interface MyHints {
Hint[] value();
}
@Repeatable(MyHints.class)
@interface Hint {
String value();
}
使用包装类当容器来存多个注解(旧版本方法)
@MyHints({@Hint("hint1") @Hint("hint2")})
class Person {}
使用多重注解(新方法)
@Hint("hint1")
@Hint("hint2")
class Person {}
2)完整类测试如下所示。
public class RepeatingAnnotations {
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Filters {
Filter[] value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Filters.class)
public @interface Filter {
String value();
}
@Filter("filter1")
@Filter("filter2")
public interface Filterable {
}
public static void main(String[] args) {
for (Filter filter : Filterable.class.getAnnotationsByType(Filter.class)) {
System.out.println(filter.value());
}
}
}
输出结果:
filter1
filter2
分析:
注释Filter被@Repeatable( Filters.class )注释。Filters 只是一个容器,它持有Filter 编译器尽力向程序员隐藏它的存在。通过这样的方式,Filterable接口可以被Filter注释两次。
另外,反射的API提供一个新方法getAnnotationsByType() 来返回重复注释的类型(注意Filterable.class.getAnnotation( Filters.class )将会返回编译器注入的Filters实例。
3)java 8之前也有重复使用注解的解决方案,但可读性不好。
public @interface MyAnnotation {
String role();
}
public @interface Annotations {
MyAnnotation[] value();
}
public class RepeatAnnotationUseOldVersion {
@Annotations({@MyAnnotation(role="Admin") @MyAnnotation(role="Manager")})
public void doSomeThing(){
}
}
Java8的实现方式(由另一个注解来存储重复注解,在使用时候,用存储注解Authorities来扩展重复注解),可读性更强。
@Repeatable(Annotations.class)
public @interface MyAnnotation {
String role();
}
public @interface Annotations {
MyAnnotation[] value();
}
public class RepeatAnnotationUseOldVersion {
@MyAnnotation(role="Admin")
@MyAnnotation(role="Manager")
public void doSomeThing(){
}
}
类型注解(ElementType.TYPE_PARAMETER)
1)Java 8 的类型注解扩展了注解使用的范围。
在java 8之前,注解只能是在声明的地方所使用,java8开始,注解可以应用在任何地方。
例如:
创建类实例
new @Interned MyObject();
类型映射
myString = (@NonNull String) str;
implements 语句中
class UnmodifiableList<T> implements@Readonly List<@Readonly T> { ... }
throw exception声明
void monitorTemperature() throws@Critical TemperatureException { ... }
注意:
在Java 8里面,当类型转化甚至分配新对象的时候,都可以在声明变量或者参数的时候使用注解。
Java注解可以支持任意类型。
类型注解只是语法而不是语义,并不会影响java的编译时间,加载时间,以及运行时间,也就是说,编译成class文件的时候并不包含类型注解。
2)新增ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER(在Target上)
新增的两个注释的程序元素类型 ElementType.TYPE_USE 和 ElementType.TYPE_PARAMETER用来描述注解的新场合。
- ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中。
- ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中(例如:声明语句、泛型和强制转换语句中的类型)。
例如,下面的示例。
@Target({ElementType.TYPE_PARAMETER ElementType.TYPE_USE})
@interface MyAnnotation {}
3)类型注解的作用
类型注解被用来支持在Java的程序中做强类型检查。配合第三方插件工具Checker Framework(注:此插件so easy 这里不介绍了),可以在编译的时候检测出runtime error(例如:UnsupportedOperationException;NumberFormatException;NullPointerException异常等都是runtime error),以提高代码质量。这就是类型注解的作用。
注意:使用Checker Framework可以找到类型注解出现的地方并检查。
例如下面的代码。
import checkers.nullness.quals.*;
public class TestDemo{
void sample() {
@NonNull Object my = new Object();
}
}
使用javac编译上面的类:(当然若下载了Checker Framework插件就不需要这么麻烦了)
javac -processor checkers.nullness.NullnessChecker TestDemo.java
上面编译是通过的,但若修改代码:
@NonNull Object my = null;
但若不想使用类型注解检测出来错误,则不需要processor,正常javac TestDemo.java是可以通过编译的,但是运行时会报 NullPointerException 异常。
为了能在编译期间就自动检查出这类异常,可以通过类型注解结合 Checker Framework 提前排查出来错误异常。
注意java 5 6 7版本是不支持注解@NonNull,但checker framework 有个向下兼容的解决方案,就是将类型注解@NonNull 用/**/注释起来。
import checkers.nullness.quals.*;
public class TestDemo{
void sample() {
/*@NonNull*/ Object my = null;
}
}
这样javac编译器就会忽略掉注释块,但用checker framework里面的javac编译器同样能够检测出@NonNull错误。
通过 类型注解 checker framework 可以在编译时就找到runtime error。
注解支持继承吗?使用注解@Inherited可以让指定的注解在某个类上使用后,这个类的子类自动被该注解标记
注解没有@Inherited |
注解有@Inherited原注解 | |
子类能否继承到父类的类注解 |
否 |
能 |
子类重写或实现父类的方法,该方法能否继承父类方法上的注解 |
否 |
否 |
子类继承了父类的方法,这个方法能否继承到注解 |
能 |
能 |
结论:我们知道在编写自定义注解时,可以通过指定@Inherited注解,指明白定义注解是否可以被继承
编写类注解验证
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Dengyi {
String value() default "dengyi";
}
@Dengyi
public class Person {}
public class Student extends Person {
public static void main(String[] args) {
boolean res = Student.class.isAnnotationPresent(Dengyi.class); //true
}
}
注意注解继承的传递性:
如果被标注为@Inherited某注解 用在一个父类上,则其子类,孙子类都可以通过反射获取该注解
@Target({ElementType.TYPE ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Dengyi{
String value() default "";
}
@Dengyi
public class A {
}
public class B extends A{
}
public class C extends B{
public static void main(String[] args){
Class<?> clz = C.class;
Dengyi dengyi = clz.getAnnotation(Dengyi.class);
System.out.println(dengyi);
}
}
重写或实现方法会覆盖掉原有方法的注解
这个应当这样理解
public interface Map{
@Dengyi
void put();
}
public class HashMap implements Map{
@Override
public void put(){ //这里相当于把接口的方法想覆盖掉了,当然就没有原来方法的注解了
}
public static void main (String[] args) throws NoSuchMethodException{
Class<HashMap> clz = HashMap.class;
Method put = clz.getMethod("put");
Dengyi annotation = put.getAnnotation(Dengyi.class);
System.out.println(annotation); //output: null
Class<Map> clz2 = Map.class;
Method put2 = clz2.getMethod("put")
Dengyi dengyi = put2.getAnnotation(Dengyi.class);
System.out.println(dengyi); //output: @com.jianglei3.bean.Dengyi(value=)
}
}
子类继承父类的方法会继承注解
因为子类掉用父类方法时,会去父类寻找该方法的信息
public class MMap{
@Dengyi
public void put(){}
}
public class HashMap extend MMap{
public static void main(String[] args) throws NoSuchMethodException{
Method method = MMap.class.getMethod("put");
Annotation dengyi = method.getAnnotation(Dengyi.class);
System.out.println(dengyi); //output: @com.jianglei3.bean.Dengyi(value=)
}
注解实现的原理?
设计到字节码知识,可以参考我的这篇文章:
深入理解JVM虚拟机——java字节码技术及其命令剖析
1.log日志,特殊日志可以使用注解进行记录;
环境搭建
创建一个spring boot项目,并引入spring aop
项目中的pom.xml内容为
<dependencies>
<!-- SpringBoot 拦截器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatisplus-spring-boot-starter</artifactId>
<version>${mybatisplus-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generate</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!--阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
编写自定义日志注解
在合适的包下创建自定义注解BussinessLog
/**
* 标记需要做业务日志的方法
*
* @author earthchen
* @date 2018/8/24
**/
@Target({ElementType.PARAMETER ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BussinessLog {
/**
* 日志类型
*
* @return
*/
String type() default "";
/**
* 业务的名称 例如:"修改菜单"
*/
String value() default "";
}
如果还需要其他的参数可以自定义其他方法
编写日志逻辑
创建一个日志切面
import com.alibaba.fastjson.JSONObject;
import com.earthchen.constant.BusinessStatus;
import com.earthchen.domain.OperationLog;
import com.earthchen.log.AsyncFactory;
import com.earthchen.log.LogManager;
import com.earthchen.log.annotation.BussinessLog;
import com.earthchen.utils.HttpUtil;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Map;
/**
* 日志切面
*
* @author earthchen
* @date 2018/8/24
**/
@Aspect
@Component
@EnableAsync
public class LogAop {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Pointcut(value = "@annotation(com.earthchen.log.annotation.BussinessLog)")
public void logPointCut() {
}
/**
* 前置通知 用于拦截操作
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "logPointCut()")
public void doBefore(JoinPoint joinPoint) {
handleLog(joinPoint null);
}
/**
* 拦截异常操作
*
* @param joinPoint
* @param e
*/
@AfterThrowing(value = "logPointCut()" throwing = "e")
public void doAfter(JoinPoint joinPoint Exception e) {
handleLog(joinPoint e);
}
@Async
protected void handleLog(final JoinPoint joinPoint final Exception e) {
try {
// 获得注解
BussinessLog controllerLog = getAnnotationLog(joinPoint);
if (controllerLog == null) {
return;
}
// 获取当前的用户
// User currentUser = ShiroUtils.getUser();
// *========数据库日志=========*//
OperationLog operLog = new OperationLog();
operLog.setStatus(BusinessStatus.SUCCESS);
operLog.setMessage("操作成功");
operLog.setCreatetime(new Date());
// 请求的地址
operLog.setOperUrl(HttpUtil.getRequest().getRequestURI());
if (e != null) {
operLog.setStatus(BusinessStatus.FAIL);
operLog.setMessage(StringUtils.substring(e.getMessage() 0 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className "." methodName "()");
// 处理设置注解上的参数
getControllerMethodDescription(controllerLog operLog);
// 保存数据库
LogManager.me().executeLog(AsyncFactory.bussinessLog(operLog));
} catch (Exception exp) {
// 记录本地异常日志
log.error("==前置通知异常==");
log.error("异常信息:{}" exp.getMessage());
exp.printStackTrace();
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param log
* @param operLog
* @throws Exception
*/
public void getControllerMethodDescription(BussinessLog log OperationLog operLog) throws Exception {
// 设置日志类型
operLog.setLogtype(log.type());
// 设置日志名字
operLog.setLogname(log.value());
// 获取参数的信息,传入到数据库中。
setRequestValue(operLog);
}
/**
* 获取请求的参数,放到log中
*
* @param operLog
*/
private void setRequestValue(OperationLog operLog) {
Map<String String[]> map = HttpUtil.getRequest().getParameterMap();
String params = JSONObject.toJSONString(map);
operLog.setOperParams(params);
}
/**
* 是否存在注解,如果存在就获取
*
* @param joinPoint
* @return
* @throws Exception
*/
private BussinessLog getAnnotationLog(JoinPoint joinPoint) throws Exception {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(BussinessLog.class);
}
return null;
}
}
- 这里定义的切点定义是所有被BussinessLog注解的方法上,如果有其他需求也可以自定义
- 这里还是用了@EnableAsync和@Async注解,使其在打日志的时候是异步的
- 由于异步交给线程池处理,在线程中不能直接获取spring中的bean,所以需要借助springUtil获取相关bean进行操作
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
/**
* spring工具类 方便在非spring管理环境中获取bean
*
* @author earthchen
* @date 2018/8/24
**/
@Component
public final class SpringUtils implements BeanFactoryPostProcessor {
/**
* Spring应用上下文环境
*/
private static ConfigurableListableBeanFactory beanFactory;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
SpringUtils.beanFactory = beanFactory;
}
/**
* 获取对象
*
* @param name
* @return Object 一个以所给名字注册的bean的实例
* @throws org.springframework.beans.BeansException
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException {
return (T) beanFactory.getBean(name);
}
/**
* 获取类型为requiredType的对象
*
* @param clz
* @return
* @throws org.springframework.beans.BeansException
*/
public static <T> T getBean(Class<T> clz) throws BeansException {
T result = (T) beanFactory.getBean(clz);
return result;
}
/**
* 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
*
* @param name
* @return boolean
*/
public static boolean containsBean(String name) {
return beanFactory.containsBean(name);
}
/**
* 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
*
* @param name
* @return boolean
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*/
public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
return beanFactory.isSingleton(name);
}
/**
* @param name
* @return Class 注册对象的类型
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*/
public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
return beanFactory.getType(name);
}
/**
* 如果给定的bean名字在bean定义中有别名,则返回这些别名
*
* @param name
* @return
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*/
public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
return beanFactory.getAliases(name);
}
}
编写controller进行测试
import com.earthchen.log.annotation.BussinessLog;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author earthchen
* @date 2018/8/24
**/
@RestController
public class TestController {
@BussinessLog(type = "操作" value = "进行测试")
@RequestMapping("/test")
public String testLog(){
return "test";
}
}
运行项目,然后访问上述controller,然后查看控制台和数据库中相应的表是否有对应数
2.注解的应用之监控方法执行耗时
假如,我们需要监控某些方法的执行,最原始的办法就是在方法执行的开头和结尾分别记录时间,最后计算前后的时间差即可,但是这些代码与核心业务无关,且大量重复、分散在各处,维护起来也困难。这时我们可以使用Spring AOP来统计方法的执行耗时,同时我们也可以使用注解的方式来实现,更自由灵活。
首先,定义我们的执行耗时的方法上的注解:
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;
/**
* 自定义'统计方法耗时'并打印日志的注解.
*
* @author blinkfox on 2017-01-04.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface CostTime {
/**
* 执行超过某毫秒数时数则打印'warn'级别的日志,默认 0ms,即默认都打印.
*
* @return 毫秒数
*/
long value() default 0;
}
然后,书写监控所标注有@CostTime注解的方法代理类:
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 被标注为'@CostTime'注解的方法执行耗时的代理方法.
* <p>实现了cglib中的`MethodInterceptor`的方法拦截接口.</p>
*
* @author blinkfox on 2017-01-04.
*/
public class CostTimeProxy implements MethodInterceptor {
private static final Logger log = LoggerFactory.getLogger(CostTimeProxy.class);
private Enhancer enhancer = new Enhancer();
/**
* 获取代理类.
*
* @param cls 代理类的class
* @return 代理类实例
*/
public Object getProxy(Class cls) {
enhancer.setSuperclass(cls);
enhancer.setCallback(this);
return enhancer.create();
}
/**
* 拦截方法 判断是否有'@CostTime'的注解,如果有则拦截执行.
*
* @param o 对象
* @param method 方法
* @param args 参数
* @param methodProxy 代理方法
* @return 对象
* @throws Throwable 问题
*/
@Override
public Object intercept(Object o Method method Object[] args MethodProxy methodProxy) throws Throwable {
// 判断该方法上是否有 CostTime 注解
if (!method.isAnnotationPresent(CostTime.class)) {
return methodProxy.invokeSuper(o args);
}
// 获取注解信息
CostTime costTime = method.getAnnotation(CostTime.class);
long limitTime = costTime.value();
// 记录方法执行前后的耗时时间,并做差,判断是否需要打印方法执行耗时
long startTime = System.currentTimeMillis();
Object result = methodProxy.invokeSuper(o args);
long diffTime = System.currentTimeMillis() - startTime;
if (limitTime <= 0 || (diffTime >= limitTime)) {
String methodName = method.getName();
// 打印耗时的信息
log.warn("【CostTime监控】通过注解监控方法'{}'的执行耗时为:{}" methodName diffTime);
}
return result;
}
}
接着,可以写一些业务类及方法,这里就以A类为例:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A类.
*
* @author blinkfox on 2017/1/1.
*/
public class A {
private static final Logger log = LoggerFactory.getLogger(A.class);
/**
* 始终打印方法执行耗时的方法.
*/
@CostTime
public void doSomeThing() {
log.info("执行A类中doSomeThing()方法!");
}
/**
* 当方法执行耗时大于等于'50ms'时打印出方法执行耗时.
*/
@CostTime(50)
public void doSomeThing2() {
log.info("执行A类中doSomeThing2()方法!");
}
}
最后,是用来测试A类某些业务方法执行耗时的测试类:
package com.blinkfox.test.reflect;
/**
* 耗时注解使用测试示例
* Created by blinkfox on 2017-01-04.
*/
public class CostTimeTest {
/** A类的全局实例. */
private static A a;
static {
CostTimeProxy aproxy = new CostTimeProxy();
a = (A) aproxy.getProxy(A.class);
}
/**
* main 方法.
*
* @param args 数组参数
*/
public static void main(String[] args) {
a.doSomeThing();
a.doSomeThing2();
}
}
3.强一致业务场景下数据库查询强制走主库
传统的数据库主从架构,主从同步是有一定的延迟的,在高并发强一致性业务场景下,这种延迟是不可接受的,所以我们通过注解对一些重要的查询进行强制走主库的操作。
首先我们的数据库是分库分表的实现,中间件使用的是shardingsphere
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
编写注解
/**
* @author: lekaijun
* @date: 2021/5/24
* @description:
*/
@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface MasterSelect {
}
注解实现
/**
* 强制走主库拦截器
*
* @author:
* @description:
*/
@Aspect
@Component
public class MasterSelectAspect {
@Value("${select.need.master:false}")
private String needMaster;
@Around("@annotation(com.ewt360.strategymarket.service.aop.MasterSelect)")
public Object setMasterSelect(ProceedingJoinPoint joinPoint) throws Throwable {
//获取方法签名
MethodSignature methodSign = (MethodSignature) joinPoint.getSignature();
MasterSelect annotation = methodSign.getMethod().getAnnotation(MasterSelect.class);
if (null == annotation || !"true".equals(needMaster)) {
return joinPoint.proceed();
}
Object retVal = null;
Throwable currentThrowable = null;
try {
if (!HintManager.isMasterRouteOnly()) {
HintManager.getInstance().setMasterRouteOnly();
}
retVal = joinPoint.proceed();
} catch (Throwable throwable) {
currentThrowable = throwable;
} finally {
HintManager.clear();
if (currentThrowable != null) {
throw currentThrowable;
}
}
return retVal;
}
}
使用实例
@MasterSelect
@Override
public List<UserMissionVO> batchSubTaskInfo(BatchSubTaskInfoDTO batchSubTaskInfoDTO) {
//业务代码
}