一、 为什么写这篇文章
学习代理模式,横向学习了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)注解的方法可以有默认值。
有了以上的知识储备,我们很容易写一个注解类出来。
package com.ssi.web.helper.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Created by jay.zhou on 2018/4/20. */ @Retention(RetentionPolicy.RUNTIME)//可以使用反射获取到这个注解 @Target(ElementType.METHOD) //应用的方法上 public @interface MyServiceLog { long code() default 404L;//状态码,默认是404L错误 String description() default "无法找到网页";//描述 VisitType type();//使用枚举类型,是PC端还是APP端 }
package com.ssi.web.helper.annotation; /** * Created by jay.zhou on 2018/4/20. */ public enum VisitType{ //每一个实例对象都是 public static final VisitType WEB = new VisitType("PC端"); 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对象,就能只要拿到这个类中所有的注解类的对象,就能获取此注解类对象的这些属性,就像下面这样操作。
package com.ssi.web.helper.annotation; import java.lang.reflect.Method; /** * Created by jay.zhou on 2018/4/20. */ 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端 */ } }
因此,如果我们有办法获取到一个类的字节码文件,就能操作这个类中所有有注解的属性,比如方法,字段。
我在实际工作中,大佬用的是Spring提供的切面类,JoinPoint这个对象,能过获取到某些方法的字节码。今天先到这里,待我学习一波AOP操作后,再来继续写下面的内容。
分割线--------------------------------------------------------------------------------------------
四、切面类知识储备
关于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对象。有了反射对象,这个方法就透明了。3和4就像这样:我先定义找到干活的目标地方,然后考虑去那个地方干嘛。可以获取业务方法参数的方法,等以后需要的话可以参考:基于@AspectJ的AOP ——跟我学spring3
有一个超级牛X的环绕通知在我红色的书上,五一回去看,比JoinPoint还厉害的,能获取到更多的参数。
分割线--------------------------------------------------------------------------------------------
JoinPoint 对象
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.
常用api:
方法名 | 功能 |
---|---|
Signature getSignature(); | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 |
Object[] getArgs(); | 获取传入目标方法的参数对象 |
Object getTarget(); | 获取被代理的对象 |
Object getThis(); | 获取代理对象 |
ProceedingJoinPoint对象
ProceedingJoinPoint对象是JoinPoint的子接口,该对象只用在@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,对它只有印象,但是不知道怎么用,现在在项目里面,经过实际接触才知道它是用来干嘛的。
分割线--------------------------------------------------------------------------------------------
下一篇:设计模式到底有多少个好处11