Java 代理学习笔记
  5pInOvsngCsu 2024年08月07日 23 0

公司的项目找 bug 的时候每次都是手动打日志, 项目部署时间很长, 效率实在是太低了。 这些日志与逻辑无关但是常用的库又没有提供, 非常影响代码的可阅读性。 于是我就在想可不可以利用 SpringBoot 的 aop 来抽象一层切面打印常用日志。于是就捡起了被我丢掉的代理相关知识, 这里做个总结方便以后查阅。

代理分类

  • 静态代理
  • 动态代理
    • JDK 动态代理
    • CGLIB 动态代理

静态代理

静态代理比较简单, 被代理的类被做为代理类的一个属性维护, 代理类通过在方法中调用目标对象的方法来实现功能,但可以在调用前后添加额外的逻辑,如日志记录、安全检查、事务管理等。所以静态代理是在编译期就确定的。

需要注意的是, 一般情况下, 推荐代理类和被代理类实现同一套接口, 这样做的好处是可以避免代理类没有编写必要的方法。

示例代码

  1. 首先定义一个接口, 规定需要哪些方法
public interface SubjectI {
	String test(String a);
}
  1. 定义实现类
public class SubjectImpl implements SubjectI {

	@Override
	public String test(String a) {
		return "hello world, " + a;
	}
}
  1. 定义代理类, 将被代理类做为代理类的属性
public class SubjectProxy implements SubjectI {
	private SubjectI subject;
	
	public SubjectProxy(SubjectI subject) {
		this.subject = subject;
	}

	@Override	
	public String test(String a) {
		System.out.println("[test ----> start]");
		System.out.println("[test ----> param], a=" + a);
		String result = subject.test(a);
		System.out.println("[test ----> return] " + result);
		return result;
	}
}
  1. 定义主类测试
public class Main {
    public static void main(String[] args) {
        SubjectProxy subjectProxy = new SubjectProxy(new SubjectImpl());
        subjectProxy.test("hhh");
    }
}

输出如下:

[test ----> start]
[test ----> param], a=hhh
[test ----> return] hello world, hhh

优点

  • 实现简单
  • 编译时检查, 编译时就能检查出代码中的一些错误
  • 性能较高, 静态代理在编译时已经确定,不需要运行时的额外开销,所以运行时性能相对较高。

缺点

  • 不灵活, 无法在运行时动态地改变代理行为,在动态性要求较高的场景下不如动态代理灵活。
  • 维护困难, 代码的扩展性和维护性较差,如果要为新的接口或者功能添加代理,需要手动编写相应的代理类。

JDK 动态代理

JDK动态代理是Java提供的一种在运行时创建代理对象的机制。允许在不修改原始类代码的情况下为原始对象创建一个代理,从而实现方法拦截和增强。JDK动态代理主要依赖于java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。

以下是JDK动态代理的主要步骤:

  1. 定义接口:代理类将会实现这个接口。
  2. 实现InvocationHandler接口:创建一个实现InvocationHandler接口的类,重写invoke方法,在这个方法中描述你希望在调用目标方法时执行的逻辑。
  3. 使用Proxy类生成代理对象:使用Proxy.newProxyInstance方法生成代理对象。

示例代码

假设有一个接口HelloService及其实现类HelloServiceImpl

public interface HelloService {
    String sayHello(String from, String to, int count);
}
public class HelloServiceImpl implements HelloService {
    @Override
    public String sayHello(String name, String to, int count) {
        for (int i = 0; i < count; i++) {
            System.out.printf("%s: hello, %s\n", name, to);
        }
        return name + ": hello, " + to;
    }
}

接下来是代理的实现:

package com.code.spring2test.JDKDynamicProxy;

import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import jdk.nashorn.internal.parser.JSONParser;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.HashMap;

public class HelloServiceProxy implements InvocationHandler {

    private Object target;

    public HelloServiceProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.printf("[%s ----> start]\n", method.getName());

        Parameter[] parameters = method.getParameters();
        HashMap<String, Object> params = new HashMap<>();
        for (int i = 0; i < parameters.length; i++) {
            params.put(parameters[i].getName(), args[i]);
        }
        System.out.printf("[%s ----> param] %s\n", method.getName(), JSONUtil.toJsonPrettyStr(params));

        Object result = method.invoke(target, args);
        System.out.printf("[%s ----> return] %s\n", method.getName(), result);

        return result;
    }

    public static void main(String[] args) {
        // 创建原始对象
        HelloServiceImpl helloService = new HelloServiceImpl();

        // 创建InvocationHandler
        HelloServiceProxy proxyHandler = new HelloServiceProxy(helloService);

        // 创建代理对象
        HelloService proxyInstance = (HelloService) Proxy.newProxyInstance(
                helloService.getClass().getClassLoader(),
                helloService.getClass().getInterfaces(),
                proxyHandler
        );

        // 调用代理对象的方法
        proxyInstance.sayHello("Java", "world", 3);
    }
}


输出如下:

[sayHello ----> start]
[sayHello ----> param] {
    "count": 3,
    "from": "Java",
    "to": "world"
}
Java: hello, world
Java: hello, world
Java: hello, world
[sayHello ----> return] Java: hello, world

注意需要加上编译参数-parameters参数名才会正确显示, 不然会显示 arg0, arg1, ...

pom.xml里面 对于 maven-compiler-plugin, <configuration>标签下

<compilerArgs>
	<arg>-parameters</arg>
</compilerArgs>

动态代理机制

当使用JDK动态代理时,代理实例是通过调用Proxy.newProxyInstance方法创建的。这个方法有三个主要参数:

  • 类加载器 (ClassLoader):用于定义代理类的类加载器。
  • 一个接口数组 (Class<?>[]):代理类需要实现的接口列表。
  • 调用处理器 (InvocationHandler):拦截方法调用的处理器。
MyInterface proxyInstance = (MyInterface) Proxy.newProxyInstance(
        MyInterface.class.getClassLoader(),
        new Class<?>[]{MyInterface.class},
        invocationHandler);

Proxy.newProxyInstance方法返回的代理对象实际是JDK在运行期间生成的一个类的实例。这个生成的类实现了指定的接口,并在每个方法调用时将调用委托给InvocationHandler。这就限制了它只能作为接口的实现类。

缺点

  • 只能代理实现了接口的类
  • 基于反射, 性能相对较差

优点

  • JDK 自带, 天然支持, 简单易用

CGLIB动态代理

CGLIB(Code Generation Library)是一个强大的、高性能的代码生成库,它可以在运行时为接口和类生成代理对象。与Java自带的动态代理(只能代理实现了接口的类)不同,CGLIB可以代理没有实现接口的类,因为它是通过生成目标类的子类并覆盖其中的方法来实现代理的。

示例代码

package com.code.spring2test.CGLIB;

import cn.hutool.json.JSONUtil;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;

public class CglibDemo {
    public static class RealService {
        public String sayHello(String from, String to, int count) {
            for (int i = 0; i < count; i++) {
                System.out.println(from + ": hello, " + to);
            }
            return from + ": hello, " + to;
        }
    }

    public static class ServiceInterceptor implements MethodInterceptor {
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            System.out.printf("[%s ----> start]\n", method.getName());

            Parameter[] parameters = method.getParameters();
            HashMap<String, Object> params = new HashMap<>();
            for (int i = 0; i < parameters.length; i++) {
                params.put(parameters[i].getName(), args[i]);
            }
            System.out.printf("[%s ----> param] %s\n", method.getName(), JSONUtil.toJsonPrettyStr(params));

            Object result = proxy.invokeSuper(obj, args);
            System.out.printf("[%s ----> return] %s\n", method.getName(), result);

            return result;
        }
    }

    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(RealService.class);
        enhancer.setCallback(new ServiceInterceptor());

        RealService proxy = (RealService) enhancer.create();
        proxy.sayHello("Java", "world", 3);
    }
}

输出如下

[sayHello ----> start]
[sayHello ----> param] {
    "count": 3,
    "from": "Java",
    "to": "world"
}
Java: hello, world
Java: hello, world
Java: hello, world
[sayHello ----> return] Java: hello, world

MethodProxy.invokeSuper

通常情况下,MethodProxy.invokeSuper 的性能会好于原生的 Method.invoke

原因:

  1. 字节码操作MethodProxy.invokeSuper 是 cglib 动态代理库提供的方法,cglib 通过生成字节码直接调用父类的方法,这种方式比反射调用的方法更加直接,因此性能更高。

  2. 反射开销Method.invoke 使用的是 Java 的反射机制,反射机制会带来额外的性能开销,例如方法查找、访问控制检查等。相较之下,MethodProxy.invokeSuper 使用预生成的字节码,可以跳过这些步骤。

  3. 优化可能性:动态代理库如 cglib 通常会进行性能优化,而这些优化是基于生成的字节码实现的,进一步提升了调用的效率。

缺点

  • 需要使用第三方依赖, 使用上相对复杂
  • 被代理的类不能是 final,否则会抛出异常,因为Cglib通过继承来生成代理类。
  • 生成代理类需要一定的字节码操作开销,可能会影响性能, 有更高的内存消耗。

优点

  • 与JDK动态代理相比, 无需接口
  • 调用方法时是使用的MethodProxy.invokeSuper, 相较于原生的Method.invoke性能更好
  • 功能强大, Cglib可以实现比静态代理和JDK动态代理更复杂的代理行为。

总结

  • 静态代理适用于简单场景,但不灵活,难以维护。
  • JDK动态代理适用于需要代理接口的场景,简单且开发成本低。
  • Cglib代理适用于需要代理没有接口的类或追求更高性能的场景,但复杂度较高。

FAQ

SpringBoot 中 AOP 的代理方式

在 Spring Boot 的 AOP(面向切面编程)中,有两种主要的代理方式:JDK 动态代理和 CGLIB 代理。

  1. JDK 动态代理

    • 使用的是 Java 提供的 java.lang.reflect.Proxy 类。
    • 代理对象必须实现一个或多个接口。
    • 这种方式的主要优点是简单和轻量,但缺点是只能代理接口方法。
  2. CGLIB 代理

    • 使用的是 net.sf.cglib.proxy.Enhancer 类。
    • 不要求目标对象实现接口,可以代理实际类的方法。
    • CGLIB 通过生成目标类的子类来创建代理,对类的方法进行覆盖,因此不能代理 final 方法。

在 Spring 中,默认情况下会优先使用 JDK 动态代理,如果目标对象没有实现任何接口,才会使用 CGLIB 代理。

在 Spring 中默认会优先使用 JDK 动态代理,而 JDK 动态代理要求目标对象实现接口。如果已经定义了接口,AOP 的代理机制会更加简洁和高效。所以这也是写 service 先写一个接口的部分原因

可以通过以下方式指定使用 CGLIB 代理:

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
    // 其他 Bean 配置
}

这里的 proxyTargetClass = true 表示强制使用 CGLIB 代理。

通过这两种代理方式,Spring AOP 实现了对方法的拦截和增强,使得在不改变原来的业务代码的前提下添加额外的功能。

拓展

SpringBoot 中使用 AOP 的几种方式

自定义注解
  1. 自己写一个注解
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 EnableLog {
}
  1. 对于这个注解写一个切面处理
@Aspect
@Component
@Slf4j
public class LogAspect {
    @Around("@annotation(EnableLog)")
    public Object aroundOperateLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Signature signature = proceedingJoinPoint.getSignature();
        if (!(signature instanceof MethodSignature)) {
            return new Object();
        }
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        targetMethod.setAccessible(true);

        Object resVal = null;
        String methodName = targetMethod.getName();
        Object[] methodArgs = proceedingJoinPoint.getArgs();
        String[] parameterNames = methodSignature.getParameterNames();

        // 将参数名和参数值配对
        HashMap<String, Object> params = new HashMap<>();
        for (int i = 0; i < parameterNames.length; i++) {
            params.put(parameterNames[i], methodArgs[i]);
        }

        // 执行前打印方法名和参数
        log.info("[function {} ----> start] {}", methodName, JSONUtil.toJsonStr(params));

        try {
            resVal = proceedingJoinPoint.proceed();
            // 执行成功后打印结果
            log.info("[function {} ----> end] {}", methodName, resVal);
        } catch (Throwable throwable) {
            // 执行失败时打印异常
            log.info("[function {} ----> error] {}", methodName, throwable.getMessage());
            throw throwable;  // 重新抛出异常以便调用方处理
        }

        return resVal;
    }
}

匹配某个路径
  • @Pointcut:声明一个切点。切点用于指定哪些方法会被拦截(advice)应用到。它实际上定义了一种规则,以便Spring可以找到何时以及在哪里应用不同的advice(比如方法执行前、执行后、抛出异常时等)。

  • execution:这个关键字用于指示拦截的时机,execution特别是指方法执行时。这是指定切点的最常用方法之一,因为它可以非常具体地指明哪些方法将被拦截。

  • * com.code.spring2test..*(..):这部分进一步细分了execution中的模式,具体分解如下:

    • *:这是一个通配符,表示返回任何类型。这意味着不论方法的返回类型是什么,都将匹配该模式。
    • com.code.spring2test..:这指定了类的完全限定名或包名。两个点..表示该包及其所有子包。因此,这表明任何在包com.code.spring2test及其子包下的类都将被考虑在内。
    • *:这又是一个通配符,位于方法名的位置。表示匹配该包下的任何类中的任何方法。
    • (..):这表示方法参数,括号中的两个点表示匹配任何数量、任何类型的参数。
@Aspect
@Component
@Slf4j
public class LogAspect {

    @Pointcut("execution(* com.code.spring2test..*(..))")
    public void allMethod(){}

    @Around("allMethod()")
    public Object aroundOperateLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Signature signature = proceedingJoinPoint.getSignature();
        if (!(signature instanceof MethodSignature)) {
            return new Object();
        }
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        targetMethod.setAccessible(true);

        Object resVal = null;
        String methodName = targetMethod.getName();
        Object[] methodArgs = proceedingJoinPoint.getArgs();
        String[] parameterNames = methodSignature.getParameterNames();

        // 将参数名和参数值配对
        HashMap<String, Object> params = new HashMap<>();
        for (int i = 0; i < parameterNames.length; i++) {
            params.put(parameterNames[i], methodArgs[i]);
        }

        // 执行前打印方法名和参数
        log.info("[function {} ----> start] {}", methodName, JSONUtil.toJsonStr(params));

        try {
            resVal = proceedingJoinPoint.proceed();
            // 执行成功后打印结果
            log.info("[function {} ----> end] {}", methodName, resVal);
        } catch (Throwable throwable) {
            // 执行失败时打印异常
            log.info("[function {} ----> error] {}", methodName, throwable.getMessage());
            throw throwable;  // 重新抛出异常以便调用方处理
        }

        return resVal;
    }
}

原文地址: http://mywhp.cn/blog/#/blog/31

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2024年08月07日 0

暂无评论

推荐阅读
  VGxawBTN4xmE   2天前   19   0   0 Java
  FHUfYd9S4EP5   4天前   29   0   0 Java
  u8s65Xl4dX8N   7小时前   9   0   0 Java
  qCe06rFCa8NK   7小时前   13   0   0 Java
  ZTo294hNoDcA   4天前   29   0   0 Java
  FHUfYd9S4EP5   4天前   24   0   0 Java