掘金 后端 ( ) • 2021-05-24 19:17
.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#333}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin-bottom:10px;padding-bottom:5px}.markdown-body h1{font-size:30px;margin-bottom:5px}.markdown-body h2{padding-bottom:12px;font-size:24px;border-bottom:1px solid #ececec}.markdown-body h3{font-size:18px;padding-bottom:0}.markdown-body h4{font-size:16px}.markdown-body h5{font-size:15px}.markdown-body h6{margin-top:5px}.markdown-body p{line-height:inherit;margin-top:22px;margin-bottom:22px}.markdown-body img{max-width:100%}.markdown-body hr{border:none;border-top:1px solid #ddd;margin-top:32px;margin-bottom:32px}.markdown-body code{word-break:break-word;border-radius:2px;overflow-x:auto;background-color:#fff5f5;color:#ff502c;font-size:.87em;padding:.065em .4em}.markdown-body code,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative;line-height:1.75}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#333;background:#f8f8f8}.markdown-body a{text-decoration:none;color:#0269c8;border-bottom:1px solid #d1e9ff}.markdown-body a:active,.markdown-body a:hover{color:#275b8c}.markdown-body table{display:inline-block!important;font-size:12px;width:auto;max-width:100%;overflow:auto;border:1px solid #f6f6f6}.markdown-body thead{background:#f6f6f6;color:#000;text-align:left}.markdown-body tr:nth-child(2n){background-color:#fcfcfc}.markdown-body td,.markdown-body th{padding:12px 7px;line-height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{color:#666;padding:1px 23px;margin:22px 0;border-left:4px solid #cbcbcb;background-color:#f8f8f8}.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body ol,.markdown-body ul{padding-left:28px}.markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item{list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li .task-list-item ol,.markdown-body ul li .task-list-item ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:3px}.markdown-body ol li{padding-left:6px}.markdown-body .contains-task-list{padding-left:0}.markdown-body .task-list-item{list-style:none}@media (max-width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-size:18px}}

前言

花了两个星期处理了毕业、论文以及答辩相关事项,今天回顾了一下动态代理,看了一些文章,写文章记录一下,顺便记录一下之前给项目做的时间线操作记录的aop优化。

静态代理

准备工作

先实现一个接口IHello和一个实现类HelloImpl

/**
 * @author zhn
 */
public interface IHello {
    /**
     * sayHello
     */
    void sayHello();
}
复制代码
/**
 * @author zhn
 */
public class HelloImpl implements IHello {

    /**
     * sayHello
     */
    @Override
    public void sayHello() {
        System.out.println("Hello!!");
    }
}

复制代码

实现Handler

静态代理主要实现Handler实现IHello接口,在Handler方法中定义一个代理对象。

在调用接口方法的时候调用代理对象中的方法,同时其中可以在调用代理对象前增加其他操作

/**
 * @author zhn
 */
public class Handler implements IHello {

    //代理的目标
    private IHello target;

    public Handler(IHello target){
        this.target = target;
    }
    
    /**
     * sayHello
     */
    @Override
    public void sayHello() {
        //前
        System.out.println("代理开始");
        //调用代理对象中的方法
        target.sayHello();
        //后
        System.out.println("代理完了");
    }
}
复制代码

使用静态代理

用main方法来示例,首先创建需要代理的对象,然后把该代理对象传入Handler(即代理)中,调用Handler(代理)中的方法。

/**
 * @author zhn
 */
public class Main {
    public static void main(String[] args) {
        HelloImpl hello = new HelloImpl();
        Handler handler = new Handler(hello);
        handler.sayHello();
    }
}
复制代码

结果和总结

image.png

静态代理的实现很简单,但也很容易看出缺点,即当需要代理的类很多事就会需要很多个代理类。

动态代理

准备工作

还是创建一个接口和一个实现类,这里使用静态代理中的准备工作中的类。

创建MyInvocationHandler

实现InvocationHandler接口,实现自己的Handler

/**
 * @author zhn
 */
public class MyInvocationHandler implements InvocationHandler {

    //代理目标
    private Object target;

    //构造传入
    public MyInvocationHandler(Object target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("前置");
        //反射调用
        Object res = method.invoke(target, args);
        System.out.println("后置");
        return res;
    }
}
复制代码

和静态代理很像,核心都是在实现代理对象的方法基础上扩展实现其他方法,这里使用的是反射。

使用动态代理

同样用main方法示例,一共有两种方式

方式一:

/**
 * @author zhn
 */
public class MyProxyTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //第一种
        //将JDK动态代理生成的class文件保存到本地 生成$proxy0的class文件
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        //1.获取动态代理类
        Class> proxyClass = Proxy.getProxyClass(IHello.class.getClassLoader(), IHello.class);
        //2.获取代理类的构造函数,并传入参数类型InvocationHandler
        Constructor> constructor = proxyClass.getConstructor(InvocationHandler.class);
        //3.通过构造函数创建动态代理类对象,再把自定义的MyInvocationHandler传入
        IHello iHello1 = (IHello)constructor.newInstance(new MyInvocationHandler(new HelloImpl()));
        //4.通过代理对象调用目标方法
        iHello1.sayHello();
    }
}
复制代码

方法二:Proxy中有集成了以上1~3步骤方法 直接使用更方便

/**
 * @author zhn
 */
public class MyProxyTest {
    public static void main(String[] args){
        //第二种方法
        IHello iHello2 = (IHello)Proxy.newProxyInstance(IHello.class.getClassLoader(),
                                                new Class[]{IHello.class},
                                                new MyInvocationHandler(new HelloImpl()));
        //调用方法
        iHello2.sayHello();

    }
}
复制代码

点进去看一下newProxyInstance方法

image.png

image.png h就是我们传入的MyInvocationHandler

结果和总结

image.png

稍微总结一下,动态代理是运行时通过反射机制创建的代理。核心主要实现InvocationHandler接口,从而完成我们自己想要的操作。

CGLIB

准备工作

实例实现一个HelloService类,这里使用了Spring的cglib实现,大同小异

/**
 * @author zhn
 */
public class HelloService {
    public HelloService(){
        System.out.println("HelloService构造器");
    }

    /**
     * 该方法不能被子类覆盖,cglib无法代理final修饰的方法
     */
    final public String sayOthers(){
        System.out.println("HelloService:sayOthers");
        return null;
    }

    public void sayHello(){
        System.out.println("HelloService:sayHello");
    }
}
复制代码

实现MyMethodInterceptor

实现cglib包中的MethodInterceptor接口

/**
 * @author zhn
 */
public class MyMethodInterceptor implements MethodInterceptor {

    /**
     * o:cglib生成的代理对象
     * method:被代理对象方法
     * objects:方法入参
     * methodProxy: 代理方法
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("前置");
        //调用方法 注意是调用父类的方法
        Object res = methodProxy.invokeSuper(o, objects);
        System.out.println("后置");
        return res;
    }
}
复制代码

使用代理

这里用Test示例,主要使用Enhancer类根据其给定的父类创建子类除了被 final定义外,其中在设置回调时传入我们自己的MyMethodInterceptor,从而实现功能扩展。

@SpringBootTest
public class testMyProxy {

    @Test
    public void test(){
        // 代理类class文件存入本地磁盘方便我们反编译查看源码
//        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\code");
        // 通过CGLib动态代理获取代理对象
        /**
         * Enhancer是cglib中使用频率很高的一个类,它是一个字节码增强器,
         * 可以用来为无接口的类创建代理。它的功能与java自带的Proxy类挺相似的。
         * 它会根据某个给定的类创建子类,并且所有非final的方法都带有回调钩子。
         */
        Enhancer enhancer = new Enhancer();
        //设置enhancer父类
        enhancer.setSuperclass(HelloService.class);
        //设置回调
        enhancer.setCallback(new MyMethodInterceptor());
        //创建代理对象
        HelloService proxyObject = (HelloService) enhancer.create();
        //调用方法
        proxyObject.sayHello();
    }

}
复制代码

结果和总结

image.png

CGLIB主要用字节码技术,通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,进行扩展。

JDK动态代理与CGLIB动态代理均是实现Spring AOP的基础。

字节码还没有深入研究过,看到了一篇美团文章贴一下,美团字节码文章,有空回过头来深入学习一下。

CGLIB中还有一个Fastclass机制,即上面提到的方法拦截技术,暂时还没有仔细研究,回头也需要更深了解一下。

注解+AOP

之前在项目中使用了注解+aop方式试着对一个类似于日志操作的时间轴功能做了优化,这边也记录一下。

准备工作

使用SpringBoot项目

导入aop依赖

        
            org.springframework.boot
            spring-boot-starter-aop
        
复制代码

创建一个注解TimeLine

/**
 * @author zhn
 */
@Target({ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TimeLine {

    /** 时间轴标题 */
    String subject();
    /** 时间轴描述 */
    String description() default "";

}
复制代码

@Target说明了 Annotation(java注解) 所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。

————————————————

版权声明:本文为CSDN博主「ZRHZRHH」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/ZRHZRHH/art…

Retention(保留) 注解说明,这种类型的注解会被保留到那个阶段. 有三个值:

RetentionPolicy.SOURCE —— 这种类型的Annotations只在源代码级别保留,编译时就会被忽略

RetentionPolicy.CLASS —— 这种类型的Annotations编译时被保留,在class文件中存在,但JVM将会忽略

RetentionPolicy.RUNTIME —— 这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用.

范围级别: source < class < runtime

————————————————

版权声明:本文为CSDN博主「ZRHZRHH」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/ZRHZRHH/art…

创建TimeLineAspect

加上@Component和@Aspect两个注解

其中实现撇开其他,主要为

  1. 配置切点
  2. 选择通知方法
    /**
     * 配置时间轴切点
     */
    @Pointcut("@annotation(com.example.test.annotation.TimeLine)")
    public void pointCut(){}

    /**
     * 后置通知
     * @param joinPoint 连接点
     */
    @AfterReturning(pointcut = "pointCut()")
    public void doAfter(JoinPoint joinPoint){
        //处理方法
        handleTimeLine(joinPoint);
    }
复制代码

完整代码

/**
 * @author zhn
 */
@Component
@Aspect
public class TimeLineAspect {

    /**
     * 配置时间轴切点
     */
    @Pointcut("@annotation(com.example.test.annotation.TimeLine)")
    public void pointCut(){}

    /**
     * 后置通知
     * @param joinPoint 连接点
     */
    @AfterReturning(pointcut = "pointCut()")
    public void doAfter(JoinPoint joinPoint){
        //处理方法
        handleTimeLine(joinPoint);
    }

    /**
     * 获取当前执行的方法
     *
     * @param joinPoint  连接点
     * @param methodName 方法名称
     * @return 方法
     */
    private Method currentMethod(JoinPoint joinPoint, String methodName) {
        //获取目标类的所有方法,找到当前要执行的方法

        Method[] methods = joinPoint.getTarget().getClass().getMethods();
        Method resultMethod = null;
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                resultMethod = method;
                break;
            }
        }
        return resultMethod;
    }

    /**
     * 是否存在注解,如果存在就获取
     */
    private TimeLine getAnnotationLog(Method method)
    {
        if (method != null)
        {
            return method.getAnnotation(TimeLine.class);
        }
        return null;
    }

    private void handleTimeLine(JoinPoint joinPoint){
        try {
            String name = joinPoint.getSignature().getName();
            //获取当前方法
            Method method = currentMethod(joinPoint, name);
            //获取注解
            TimeLine timeLine = getAnnotationLog(method);
            //处理...
            System.out.println("aop后置通知处理了...");
            System.out.println("方法名:"+name);
            System.out.println("注解信息: Subject=" + timeLine.Subject() +" ; description="+timeLine.description());
        }catch (Exception e){
            // 记录异常 可以加入日志记录
            e.printStackTrace();
        }
    }
}
复制代码

使用AOP

在对应的方法中添加TimeLine注解

例如:

/**
 * @author zhn
 */
@Service
public class AopTestService {

    @TimeLine(Subject = "测试aop",description = "在AopTestService类中使用testAop")
    public void testAop(){
        System.out.println("AopTestService:testAop");
    }
}
复制代码

随后只要调用该方法就会被使用TimeLineAspect中配置好的方法

结果和总结

image.png

可以看出aop的底层还是使用的代理模式,然后进行包装,最后提出了AOP面向切面编程的概念。

其中aop主要几个概念就是切点、切面、通知等。具体就不细说了。

总结

总结一下特点

静态代理最大的特点就是简单好理解

动态代理的特点就是使用反射机制

CGLIB的特点是字节码

aop的特点就是包装以上代理方法,提出aop的概念。

过几天把ElasticSearch6.8.x的实战文章写出来记录一下,然后还要忙学校的琐事(迫不及待想要毕业了)以及准备面试。

相关文章

juejin.cn/post/696305…

blog.csdn.net/ZRHZRHH/art…