一、 为什么写这篇文章
学习代理模式,横向学习了JDK动态代理,Cglib动态代理。然后学习代理模式在实际工作中的使用。现在工作中用来保存日志,使用cglib动态代理一个Controller类中的每一个方法。关于注解,我之前没有深入研究过,之前只是模仿着写过一些代码:使用自定义注解实现SpringMVC。现在已经工作了,应该增加更多的知识储备。废话不多说,开始记录。
如何开始写自己的注解类,这里推荐一篇深度好文:秒懂,Java 注解 (Annotation)你可以这样学
二、注解类知识储备
(1)创建自定义注解和创建一个接口相似,但是注解的interface关键字需要以@符号开头。
(2)每一个注解需要几个元标签。一般来说,有@Retention与@Target。
@Retention指明了这个注解应该被保留的时间。它的取值有三个。
RetentionPolicy.SOURCE 表明注解只在源码阶段保留,在进行编译时它将被丢弃忽视。
RetentionPolicy.CLASS 表明注解只在编译阶段保留,不会被加载到JVM中。
RetentionPolicy.Runtime 表明注解可以保留到运行时,可以通过各种Java反射获取到它。
通常来说,我们自己的注解几乎都用的Runtime,因为对于日志、安全、事务、缓存等公共事务,都是在项目启动以后,记录用户的操作,或者记录相关的信息。
@Target 指明这个注解应用到哪里。对一个类的剖析,无非是 字段,方法,构造函数,类。它们在java反射包下都有相应的类,比如Class,Method,Filed等。
@Target的取值有
ElementType.TYPE 这个注解可以用到类,接口,枚举类上
ElementType.METHOD 这个注解可以用到方法上
ElementType.FIELD 这个注解可以用字段上
(3)注解类中的方法不能有参数。
(4)它的返回值可以是基本数据类型,String类型,枚举类型,或者是这些类型的数组。关于枚举的知识,看样子还要学习。
(5)注解的方法可以有默认值。
有了以上的知识储备,我们很容易写一个注解类出来。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)//可以使用反射获取到这个注解
@Target(ElementType.METHOD) //应用的方法上
public @interface MyServiceLog {
long code() default 404L;//状态码,默认是404L错误
String description() default "无法找到网页";//描述
VisitType type();//使用枚举类型,是PC端还是APP端
}
public enum VisitType{
WEB("PC端"),APP("APP端");
//私有的构造函数,外部无法创建此枚举类的实例
VisitType(String type){
method = type;
}
//私有的字段
private String method;
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
}
三、使用反射获取注解的值
首先需要知道的是,所有的注解类的父类都是Annotation这个类。
注解通过反射获取。首先可以通过 Class 对象的 isAnnotationPresent() 方法判断这个类是否使用了注解。就像Spring中的@Controller,@Service,@Repository那样。
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
如何获取注解对象呢?使用getAnnotation()方法,在java.lang.reflect.Method类中与java.lang.reflect.Field类中均发现了此方法。
通过 getAnnotation() 方法来获取 Annotation 对象。如果存在该元素的指定类型的注释,则返回这些注释,否则返回 null。
public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
只要拿到这个一个类的字节码文件Class对象,就能只要拿到这个类中所有的注解类的对象,就能获取此注解类对象的这些属性,就像下面这样操作。
public class Temp {
@MyServiceLog(code = 200L , description = "响应成功",type = VisitType.WEB)
public void isAuthencated(){
//模拟是否已经被认证
}
public static void main(String[] args) throws NoSuchMethodException {
//先拿到这个类的字节码文件
Class<Temp> clazz = Temp.class;
//获取到这个类的指定方法
Method method = clazz.getMethod("isAuthencated");
//通过getAnnotatino获取到这个方法上面的注解
MyServiceLog annotation = method.getAnnotation(MyServiceLog.class);
//只要拿到了注解对象,我们就可以为所欲为
//获取状态码
Long code = annotation.code();
//获取描述
String description = annotation.description();
//获取访问方式,先获取到访问方式的枚举,然后获取枚举的值
VisitType type = annotation.type();
String type_str = type.getMethod();
//打印结果
System.out.println("状态码:"+code);
System.out.println("描述:"+description);
System.out.println("访问方式:"+type_str);
/**
* 运行结果:
* 状态码:200
* 描述:响应成功
* 访问方式:PC端
*/
}
}
因此,如果我们有办法获取到一个类的字节码文件,就能操作这个类中所有有注解的属性,比如方法,字段。
四、切面类知识储备
关于AOP的相关知识。AOP可以在以下方面受益:日志 安全 事务 缓存 性能控制。
AOP最大的好处,解耦。 把具体业务代码与非业务代码分离,降低他们的耦合。这样在写具体业务的时候,不用去考虑公共的业务。
几个专业名词的解释,仔细琢磨,不难理解的。
连接点(Jointpoint):需要被增强的某个方法,一般是业务方法。
切入点(Pointcut):多个连接点,可以视为连接点的集合。“在哪里干的集合”
关注点:增加的某个业务,如日志,安全,事务,缓存等。“干什么”
切面(Aspect):把关注点封装成类。“在哪里干和干什么的集合”
通知(Advice):在某个业务方法的前后执行。一般是增强的非业务方法,分为前置通知和后置通知等。“干什么”
AOP代理:使用JDK动态代理或者Cglib动态代理生成的代理类。
织入(Weaving):织入是一个过程,是将切面应用到目标对象从而创建出AOP代理对象的过程
在AOP中,通过切入点选择目标对象的连接点,然后在目标对象的相应连接点处织入通知。
因此,目标对象+通知 = AOP代理对象
我们写代码的过程:
1.先定义切面,@Aspect ,并且需要将其声明为bean:@Component
2.在Spring配置文件中开启动态代理。<aop:aspectj-autoproxy/>
3.定义切入点@PointCut,定义“在哪个业务方法上”面添加更多的非业务功能。这个“切入点的方法名”就是这个切入点的value。
4.定义通知Advice。前置通知使用@Before来实现,后置返回通知使用@AfterReturning实现。这些方法的参数可以定义为JointPoint,它能获取到当前方法的反射对象Method对象。有了反射对象,这个方法就透明了。
JoinPoint 对象
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.
常用api:
方法名 | 功能 |
Signature getSignature(); | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 |
Object[] getArgs(); | 获取传入目标方法的参数对象 |
Object getTarget(); | 获取被代理的对象 |
Object getThis(); | 获取代理对象 |
ProceedingJoinPoint对象
@Around的切面方法中, 添加了
Object proceed() throws Throwable //执行目标方法
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法
因此,获取被代理对象的字节码文件的操作
Class clazz = joinPoint.getTarget().getClass();
getSignature()方法是获取一个签名,返回的是一个接口的实现类。我们能够通过它获取到当前执行的方法名
String methodName = joinPoint.getSignature().getName();
获取此方法的参数
Object[] args = joinPoint.getArgs();
有了上述的三个方法,那么就非常简单了。获取到了字节码文件,获取到了方法名,并且还获取到了参数列表,这个方法完全被我们掌控。
下面提供两个比较简单的通知,一个是前置通知,另外一个是环绕通知。
//前置通知
@Before(value = "pointCutName()")
public void AdviceBefore(JoinPoint joinPoint){
LOGGER.info("前置通知开始执行");
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
LOGGER.info("当前执行的方法名是:{}",methodName);
LOGGER.info("参数是:{}", Arrays.toString(args));
}
//环绕通知Advice,它规定了在 连接点joinPoint处 “干什么”
@Around(value = "pointCutName()")
public Object AdviceAround(ProceedingJoinPoint joinPoint) throws Throwable {
//前置通知
LOGGER.info("前置通知开始");
Long startTime = System.currentTimeMillis();
final String methodName = joinPoint.getSignature().getName();
final Object[] args = joinPoint.getArgs();
LOGGER.info("方法名是:{}",methodName);
LOGGER.info("参数列表:{}",Arrays.toString(args));
Object result;
result = joinPoint.proceed();
LOGGER.info("后置通知开始");
Long endTime = System.currentTimeMillis();
LOGGER.info("本次操作执行时间为:{}毫秒",(endTime - startTime));
return result;
}
//定义一个切入点pointCut,它规定了在注解MyControllerLog处进行 织入通知操作 “在哪里干”
@Pointcut(value = "@annotation(com.ssi.web.helper.annotation.MyControllerLog)")
public void pointCutName() {
}
关于代理设计模式的探索差不多就这么多了。总有人说,你在没有经验的时候尽管埋头学,以后肯定用的上,现在终于体会到了,就像这个proceedingJoinpoint,对它只有印象,但是不知道怎么用,现在在项目里面,经过实际接触才知道它是用来干嘛的。
分割线--------------------------------------------------------------------------------------------