代理设计模式10知识扩展----注解与AOP

一、 为什么写这篇文章  

        学习代理模式,横向学习了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

        

猜你喜欢

转载自blog.csdn.net/yanluandai1985/article/details/80021111