公司的项目找 bug 的时候每次都是手动打日志, 项目部署时间很长, 效率实在是太低了。 这些日志与逻辑无关但是常用的库又没有提供, 非常影响代码的可阅读性。 于是我就在想可不可以利用 SpringBoot 的 aop 来抽象一层切面打印常用日志。于是就捡起了被我丢掉的代理相关知识, 这里做个总结方便以后查阅。
代理分类
- 静态代理
- 动态代理
- JDK 动态代理
- CGLIB 动态代理
静态代理
静态代理比较简单, 被代理的类被做为代理类的一个属性维护, 代理类通过在方法中调用目标对象的方法来实现功能,但可以在调用前后添加额外的逻辑,如日志记录、安全检查、事务管理等。所以静态代理是在编译期就确定的。
需要注意的是, 一般情况下, 推荐代理类和被代理类实现同一套接口, 这样做的好处是可以避免代理类没有编写必要的方法。
示例代码
- 首先定义一个接口, 规定需要哪些方法
public interface SubjectI {
String test(String a);
}
- 定义实现类
public class SubjectImpl implements SubjectI {
@Override
public String test(String a) {
return "hello world, " + a;
}
}
- 定义代理类, 将被代理类做为代理类的属性
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;
}
}
- 定义主类测试
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动态代理的主要步骤:
- 定义接口:代理类将会实现这个接口。
- 实现InvocationHandler接口:创建一个实现
InvocationHandler
接口的类,重写invoke
方法,在这个方法中描述你希望在调用目标方法时执行的逻辑。 - 使用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
。
原因:
-
字节码操作:
MethodProxy.invokeSuper
是 cglib 动态代理库提供的方法,cglib 通过生成字节码直接调用父类的方法,这种方式比反射调用的方法更加直接,因此性能更高。 -
反射开销:
Method.invoke
使用的是 Java 的反射机制,反射机制会带来额外的性能开销,例如方法查找、访问控制检查等。相较之下,MethodProxy.invokeSuper
使用预生成的字节码,可以跳过这些步骤。 -
优化可能性:动态代理库如 cglib 通常会进行性能优化,而这些优化是基于生成的字节码实现的,进一步提升了调用的效率。
缺点
- 需要使用第三方依赖, 使用上相对复杂
- 被代理的类不能是 final,否则会抛出异常,因为Cglib通过继承来生成代理类。
- 生成代理类需要一定的字节码操作开销,可能会影响性能, 有更高的内存消耗。
优点
- 与JDK动态代理相比, 无需接口
- 调用方法时是使用的
MethodProxy.invokeSuper
, 相较于原生的Method.invoke
性能更好 - 功能强大, Cglib可以实现比静态代理和JDK动态代理更复杂的代理行为。
总结
- 静态代理适用于简单场景,但不灵活,难以维护。
- JDK动态代理适用于需要代理接口的场景,简单且开发成本低。
- Cglib代理适用于需要代理没有接口的类或追求更高性能的场景,但复杂度较高。
FAQ
SpringBoot 中 AOP 的代理方式
在 Spring Boot 的 AOP(面向切面编程)中,有两种主要的代理方式:JDK 动态代理和 CGLIB 代理。
-
JDK 动态代理:
- 使用的是 Java 提供的
java.lang.reflect.Proxy
类。 - 代理对象必须实现一个或多个接口。
- 这种方式的主要优点是简单和轻量,但缺点是只能代理接口方法。
- 使用的是 Java 提供的
-
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 的几种方式
自定义注解
- 自己写一个注解
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 {
}
- 对于这个注解写一个切面处理
@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;
}
}