Spring高手之路17——动态代理的艺术与实践
文章目录
- 1. 背景
- 2. JDK动态代理
- 2.1 定义和演示
- 2.2 不同方法分别代理
- 2.3 熔断限流和日志监控
- 3. CGLIB动态代理
- 3.1 定义和演示
- 3.2 不同方法分别代理(对比JDK动态代理写法)
- 3.3 熔断限流和日志监控(对比JDK动态代理写法)
- 4. 动态代理图示
- 5. JDK动态代理 VS CGLIB动态代理对比
- 6. 动态代理的实际应用场景
1. 背景
动态代理是一种强大的设计模式,它允许开发者在运行时创建代理对象,用于拦截对真实对象的方法调用。这种技术在实现面向切面编程(AOP
)、事务管理、权限控制等功能时特别有用,因为它可以在不修改原有代码结构的前提下,为程序动态地注入额外的逻辑。
2. JDK动态代理
2.1 定义和演示
JDK
动态代理是Java
语言提供的一种基于接口的代理机制,允许开发者在运行时动态地创建代理对象,而无需为每个类编写具体的代理实现。
这种机制主要通过 java.lang.reflect.Proxy
类和 java.lang.reflect.InvocationHandler
接口实现。下面是JDK
动态代理的核心要点和如何使用它们的概述。
使用步骤
-
定义接口:首先定义一个或多个接口,代理对象将实现这些接口。
-
实现接口:创建一个类,它实现上述接口,提供具体的实现逻辑。
-
创建
InvocationHandler
实现:定义一个InvocationHandler
的实现,这个实现中的invoke
方法可以包含自定义逻辑。 -
创建代理对象:使用
Proxy.newProxyInstance
方法,传入目标对象的类加载器、需要代理的接口数组以及InvocationHandler
的实现,来创建一个实现了指定接口的代理对象。
用简单的例子来说明这个过程,全部代码如下:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;interface HelloWorld {void sayHello();
}class HelloWorldImpl implements HelloWorld {public void sayHello() {System.out.println("Hello world!");}
}public class DemoApplication {public static void main(String[] args) {HelloWorldImpl realObject = new HelloWorldImpl();HelloWorld proxyInstance = (HelloWorld) Proxy.newProxyInstance(HelloWorldImpl.class.getClassLoader(), // 使用目标类的类加载器new Class[]{HelloWorld.class}, // 代理类需要实现的接口列表new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 在调用目标方法前可以插入自定义逻辑System.out.println("Before method call");// 调用目标对象的方法Object result = method.invoke(realObject, args);// 在调用目标方法后可以插入自定义逻辑System.out.println("After method call");return result;}});proxyInstance.sayHello();}
}
运行结果如下:
InvocationHandler
是动态代理的核心接口之一,当我们使用动态代理模式创建代理对象时,任何对代理对象的方法调用都会被转发到一个实现了 InvocationHandler
接口的实例的 invoke
方法上。
我们经常看到InvocationHandler
动态代理的匿名内部类,这会在代理对象的相应方法被调用时执行。具体地说,每当对代理对象执行方法调用时,调用的方法不会直接执行,而是转发到实现了InvocationHandler
的 invoke
方法上。在这个 invoke
方法内部,我们可以定义拦截逻辑、调用原始对象的方法、修改返回值等操作。
在这个例子中,当调用 proxyInstance.sayHello()
方法时,实际上执行的是 InvocationHandler
的匿名内部类中的 invoke
方法。这个方法中,我们可以在调用实际对象的 sayHello
方法前后添加自定义逻辑(比如这里的打印消息)。这就是动态代理和 InvocationHandler
的工作原理。
我们来看关键的一句代码
Object result = method.invoke(realObject, args);
在Java
的动态代理中,method.invoke(realObject, args)
这句代码扮演着核心的角色,因为它实现了代理对象方法调用的转发机制。下面分别解释一下这行代码的两个主要部分:method.invoke()
方法和 args
参数。
method.invoke(realObject, args)
-
作用:这行代码的作用是调用目标对象(
realObject
)的具体方法。在动态代理的上下文中,invoke
方法是在代理实例上调用方法时被自动调用的。通过这种方式可以在实际的方法执行前后加入自定义的逻辑,比如日志记录、权限检查等。 -
method:
method
是一个java.lang.reflect.Method
类的实例,代表了正在被调用的方法。在invoke
方法中,这个对象用来标识代理对象上被调用的具体方法。
注意:如果尝试直接在invoke
方法内部使用method.invoke(proxy, args)
调用代理对象的方法,而不是调用原始目标对象的方法,则会导致无限循环。这是因为调用proxy
实例上的方法会再次被代理拦截,从而无限调用invoke
方法,传参可别传错了。
- invoke:
Method
类的invoke
方法用于执行指定方法。第一个参数是指明方法应该在哪个对象上调用(在这个例子中是realObject
),后续的参数args
是调用方法时传递的参数。
args
-
定义:
args
是一个对象数组,包含了调用代理方法时传递给方法的参数值。如果被调用的方法没有参数,args
将会是null
或者空数组。 -
作用:
args
允许在invoke
方法内部传递参数给实际要执行的方法。这意味着可以在动态代理中不仅控制是否调用某个方法,还可以修改调用该方法时使用的参数。
2.2 不同方法分别代理
我们继续通过扩展 HelloWorld
接口来包含多个方法,并通过JDK
动态代理演示权限控制和功能开关操作的一种实现方式
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;interface HelloWorld {void sayHello();void sayGoodbye();
}class HelloWorldImpl implements HelloWorld {public void sayHello() {System.out.println("Hello world!");}public void sayGoodbye() {System.out.println("Goodbye world!");}
}public class DemoApplication {public static void main(String[] args) {HelloWorld realObject = new HelloWorldImpl();HelloWorld proxyInstance = (HelloWorld) Proxy.newProxyInstance(HelloWorldImpl.class.getClassLoader(),new Class[]{HelloWorld.class},new InvocationHandler() {// 添加一个简单的权限控制演示private boolean accessAllowed = true;// 简单的功能开关private boolean goodbyeFunctionEnabled = true;@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 权限控制if (!accessAllowed) {System.out.println("Access denied");return null; // 在实际场景中,可以抛出一个异常}// 功能开关if (method.getName().equals("sayGoodbye") && !goodbyeFunctionEnabled) {System.out.println("Goodbye function is disabled");return null;}// 方法执行前的通用逻辑System.out.println("Before method: " + method.getName());// 执行方法Object result = method.invoke(realObject, args);// 方法执行后的通用逻辑System.out.println("After method: " + method.getName());return result;}});// 正常执行proxyInstance.sayHello();// 可以根据goodbyeFunctionEnabled变量决定是否执行proxyInstance.sayGoodbye();}
}
运行如下:
如果accessAllowed
变量为false
:
如果goodbyeFunctionEnabled
变量为false
:
在这个例子中:
-
权限控制:通过检查
accessAllowed
变量,我们可以模拟简单的权限控制。如果没有权限,可以直接返回或抛出异常,避免执行方法。 -
功能开关:通过检查方法名称和
goodbyeFunctionEnabled
变量,我们可以控制sayGoodbye
方法是否被执行。这可以用来根据配置启用或禁用特定功能。
这个例子展示了JDK
动态代理在实际应用中如何进行方法级别的细粒度控制,同时保持代码的灵活性和可维护性。通过动态代理,我们可以在不修改原始类代码的情况下,为对象动态地添加额外的行为。
2.3 熔断限流和日志监控
为了更全面地展示JDK
动态代理的能力,我们在先前的示例中添加熔断限流和日志监控的逻辑。这些是在高并发和分布式系统中常见的需求,可以通过动态代理以非侵入式的方式实现。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;interface HelloWorld {void sayHello();
}class HelloWorldImpl implements HelloWorld {public void sayHello() {System.out.println("Hello world!");}
}public class DemoApplication {public static void main(String[] args) {HelloWorld realObject = new HelloWorldImpl();HelloWorld proxyInstance = (HelloWorld) Proxy.newProxyInstance(HelloWorldImpl.class.getClassLoader(),new Class[]{HelloWorld.class},new AdvancedInvocationHandler(realObject));// 模拟多次调用以观察限流和熔断效果for (int i = 0; i < 10; i++) {proxyInstance.sayHello();}}static class AdvancedInvocationHandler implements InvocationHandler {private final Object target;private AtomicInteger requestCount = new AtomicInteger(0);private AtomicLong lastTimestamp = new AtomicLong(System.currentTimeMillis());private volatile boolean circuitBreakerOpen = false;private final long cooldownPeriod = 10000; // 冷却时间10秒public AdvancedInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {long now = System.currentTimeMillis();// 检查熔断器是否应该被重置if (circuitBreakerOpen && (now - lastTimestamp.get() > cooldownPeriod)) {circuitBreakerOpen = false; // 重置熔断器requestCount.set(0); // 重置请求计数System.out.println("Circuit breaker has been reset.");}// 熔断检查if (circuitBreakerOpen) {System.out.println("Circuit breaker is open. Blocking method execution for: " + method.getName());return null; // 在实际场景中,可以返回一个兜底的响应或抛出异常}// 限流检查if (requestCount.incrementAndGet() > 5) {if (now - lastTimestamp.get() < cooldownPeriod) { // 10秒内超过5次请求,触发熔断circuitBreakerOpen = true;lastTimestamp.set(now); // 更新时间戳System.out.println("Too many requests. Opening circuit breaker.");return null; // 触发熔断时的处理} else {// 重置计数器和时间戳requestCount.set(0);lastTimestamp.set(now);}}// 执行实际方法Object result = method.invoke(target, args);// 方法执行后的逻辑System.out.println("Executed method: " + method.getName());return result;}}
}
在这个扩展示例中,我们实现了:
-
熔断机制:通过一个简单的计数器和时间戳来模拟。如果在
10
秒内对任一方法的调用次数超过5
次,我们就"打开"熔断器,阻止进一步的方法调用。在实际应用中,熔断逻辑可能更加复杂,可能包括错误率的检查、调用延迟的监控等。 -
限流:这里使用的限流策略很简单,通过计数和时间戳来判断是否在短时间内请求过多。在更复杂的场景中,可以使用令牌桶或漏桶算法等更高级的限流策略。
-
日志监控:在方法调用前后打印日志,这对于监控系统的行为和性能是非常有用的。在实际项目中,这些日志可以集成到日志管理系统中,用于问题诊断和性能分析。
通过在 invoke
方法中加入这些逻辑,我们能够在不修改原有业务代码的情况下,为系统添加复杂的控制和监控功能。如果到达流量阈值或系统处于熔断状态,可以阻止对后端服务的进一步调用,直接返回一个默认值或错误响应,避免系统过载。
3. CGLIB动态代理
CGLIB
(Code Generation Library
)是一个强大的高性能代码生成库,它在运行时动态生成新的类。与JDK
动态代理不同,CGLIB
能够代理那些没有实现接口的类。这使得CGLIB
成为那些因为设计限制或其他原因不能使用接口的场景的理想选择。
3.1 定义和演示
工作原理
CGLIB
通过继承目标类并在运行时生成子类来实现动态代理。代理类覆盖了目标类的非final
方法,并在调用方法前后提供了注入自定义逻辑的能力。这种方法的一个关键优势是它不需要目标对象实现任何接口。
使用CGLIB的步骤
- 添加CGLIB依赖:首先,需要在项目中添加
CGLIB
库的依赖。
如果使用Maven
,可以添加如下依赖到pom.xml
中:
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version> <!-- 目前最新的版本 -->
</dependency>
-
创建MethodInterceptor:实现
MethodInterceptor
接口,这是CGLIB
提供的回调类型,用于定义方法调用的拦截逻辑。 -
生成代理对象:使用
Enhancer
类来创建代理对象。Enhancer
是CGLIB
中用于生成新类的类。
改造一下1.1
节的例子,可以对比看看,全部示例代码如下:
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;class HelloWorld {public void sayHello() {System.out.println("Hello world!");}
}public class DemoApplication {public static void main(String[] args) {Enhancer enhancer = new Enhancer();// 设置需要代理的类enhancer.setSuperclass(HelloWorld.class);enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable {System.out.println("Before method call");Object result = proxy.invokeSuper(obj, args); // 调用父类的方法System.out.println("After method call");return result;}});HelloWorld proxy = (HelloWorld) enhancer.create(); // 创建代理对象proxy.sayHello(); // 通过代理对象调用方法}
}
运行结果如下:
CGLIB vs JDK动态代理
- 接口要求:
JDK
动态代理只能代理实现了接口的对象,而CGLIB
能够直接代理类。 - 性能:
CGLIB
在生成代理对象时通常比JDK
动态代理要慢,因为它需要动态生成新的类。但在调用代理方法时,CGLIB
通常会提供更好的性能。 - 方法限制:
CGLIB
不能代理final
方法,因为它们不能被子类覆盖。
CGLIB
是一个强大的工具,特别适用于需要代理没有实现接口的类的场景。然而,选择JDK
动态代理还是CGLIB
主要取决于具体的应用场景和性能要求。
注意:在CGLIB
中,如果使用MethodProxy.invoke(obj, args)
,而不是MethodProxy.invokeSuper(obj, args)
,并且obj
是代理实例本身(CGLIB
通过Enhancer
创建的代理对象,而不是原始的被代理的目标对象),就会导致无限循环。invoke
方法实际上是尝试在传递的对象上调用方法,如果该对象是代理对象,则调用会再次被拦截,造成无限循环。
-
在
JDK
动态代理中,确保调用method.invoke
时使用的是目标对象,而不是代理对象。 -
在
CGLIB
代理中,使用MethodProxy.invokeSuper
而不是MethodProxy.invoke
来调用被代理的方法,以避免无限循环。
3.2 不同方法分别代理(对比JDK动态代理写法)
我们改写1.2
节的例子
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;class HelloWorldImpl {public void sayHello() {System.out.println("Hello world!");}public void sayGoodbye() {System.out.println("Goodbye world!");}
}public class DemoApplication {public static void main(String[] args) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(HelloWorldImpl.class); // 设置被代理的类enhancer.setCallback(new MethodInterceptor() {// 添加一个简单的权限控制演示private boolean accessAllowed = true;// 简单的功能开关private boolean goodbyeFunctionEnabled = true;@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {// 权限控制if (!accessAllowed) {System.out.println("Access denied");return null; // 在实际场景中,可以抛出一个异常}// 功能开关if (method.getName().equals("sayGoodbye") && !goodbyeFunctionEnabled) {System.out.println("Goodbye function is disabled");return null;}// 方法执行前的通用逻辑System.out.println("Before method: " + method.getName());// 执行方法Object result = proxy.invokeSuper(obj, args);// 方法执行后的通用逻辑System.out.println("After method: " + method.getName());return result;}});HelloWorldImpl proxyInstance = (HelloWorldImpl) enhancer.create(); // 创建代理对象proxyInstance.sayHello(); // 正常执行proxyInstance.sayGoodbye(); // 可以根据goodbyeFunctionEnabled变量决定是否执行}
}
运行结果如下:
我们需要注意几点更改:
-
因为
CGLIB
不是基于接口的代理,而是通过生成目标类的子类来实现代理,所以我们不再需要接口HelloWorld
。 -
我们将使用
Enhancer
类来创建代理实例,并提供一个MethodInterceptor
来处理方法调用。
3.3 熔断限流和日志监控(对比JDK动态代理写法)
我们改写1.3
节的例子
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;class HelloWorld {void sayHello() {System.out.println("Hello world!");}
}public class DemoApplication {public static void main(String[] args) {HelloWorld realObject = new HelloWorld();HelloWorld proxyInstance = (HelloWorld) createProxy(realObject);// 模拟多次调用以观察限流和熔断效果for (int i = 0; i < 10; i++) {proxyInstance.sayHello();}}public static Object createProxy(final Object realObject) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(HelloWorld.class);enhancer.setCallback(new AdvancedMethodInterceptor(realObject));return enhancer.create();}static class AdvancedMethodInterceptor implements MethodInterceptor {private final Object target;private final AtomicInteger requestCount = new AtomicInteger(0);private final AtomicLong lastTimestamp = new AtomicLong(System.currentTimeMillis());private volatile boolean circuitBreakerOpen = false;private final long cooldownPeriod = 10000; // 冷却时间10秒public AdvancedMethodInterceptor(Object target) {this.target = target;}@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {long now = System.currentTimeMillis();// 检查熔断器是否应该被重置if (circuitBreakerOpen && (now - lastTimestamp.get() > cooldownPeriod)) {circuitBreakerOpen = false; // 重置熔断器requestCount.set(0); // 重置请求计数System.out.println("Circuit breaker has been reset.");}// 熔断检查if (circuitBreakerOpen) {System.out.println("Circuit breaker is open. Blocking method execution for: " + method.getName());return null; // 在实际场景中,可以返回一个兜底的响应或抛出异常}// 限流检查if (requestCount.incrementAndGet() > 5) {if (now - lastTimestamp.get() < cooldownPeriod) { // 10秒内超过5次请求,触发熔断circuitBreakerOpen = true;lastTimestamp.set(now); // 更新时间戳System.out.println("Too many requests. Opening circuit breaker.");return null; // 触发熔断时的处理} else {// 重置计数器和时间戳requestCount.set(0);lastTimestamp.set(now);}}// 执行实际方法Object result = proxy.invokeSuper(obj, args); // 注意这里调用的是invokeSuper// 方法执行后的逻辑System.out.println("Executed method: " + method.getName());return result;}}
}
运行结果
在这个改写中,我们使用CGLIB
的Enhancer
和MethodInterceptor
来代替了JDK
的Proxy
和InvocationHandler
。MethodInterceptor
的intercept
方法与InvocationHandler
的invoke
方法在概念上是相似的,但它使用MethodProxy
的invokeSuper
方法来调用原始类的方法,而不是使用反射。这允许CGLIB
在运行时生成代理类的字节码,而不是依赖于反射,从而提高了性能。此外,circuitBreakerOpen
被声明为volatile
,是确保其在多线程环境中的可见性。
4. 动态代理图示
- 方法调用拦截:
客户端通过代理对象调用方法,此时方法调用被代理对象拦截。
- 转发给处理器或方法拦截器:
代理对象将方法调用转发给一个特定的处理器,这取决于所使用的代理类型。对于JDK
动态代理,这个处理器是InvocationHandler
;对于CGLIB
代理,是MethodInterceptor
。
- 执行额外操作(调用前):
在实际执行目标对象的方法之前,处理器有机会执行一些额外的操作,例如日志记录、安全检查或事务管理等。
- 调用目标对象的方法:
处理器在必要时直接调用目标对象的方法。在JDK
动态代理中,这通常通过反射实现;而在CGLIB
中,可以通过MethodProxy.invokeSuper
方法调用。
- 执行额外操作(调用后):
方法调用完成后,处理器再次有机会执行额外操作,比如修改返回值、记录执行时间或进行事务的提交或回滚。
- 返回给客户端:
最终,方法的返回值被通过代理对象返回给客户端。
5. JDK动态代理 VS CGLIB动态代理对比
JDK动态代理
JDK
动态代理是Java
自带的代理机制,它直接使用反射API
来调用方法。
优点:
-
无需第三方依赖:作为
Java
标准API
的一部分,使用JDK
动态代理不需要添加额外的库或依赖。 -
接口导向:强制使用接口进行代理,这符合面向接口编程的原则,有助于保持代码的清晰和灵活。
缺点:
-
仅限接口:只能代理实现了接口的类,这在某些情况下限制了它的使用。
-
性能开销:由于使用反射
API
进行方法调用,可能会有一定的性能开销,尤其是在大量调用时。
CGLIB动态代理
CGLIB
(Code Generation Library
)通过在运行时生成被代理对象的子类来实现代理。
优点:
-
不需要接口:可以代理没有实现任何接口的类,这提供了更大的灵活性。
-
性能较好:通常认为
CGLIB
的性能比JDK
动态代理要好,特别是在代理方法的调用上,因为CGLIB
使用了字节码生成技术,减少了使用反射的需要。
缺点:
-
第三方库:需要添加
CGLIB
库作为项目依赖。 -
无法代理final方法:由于
CGLIB
是通过生成子类的方式来代理的,所以无法代理那些被声明为final
的方法。
性能比较
-
调用速度:
CGLIB
在代理方法调用方面通常比JDK
动态代理更快。这是因为CGLIB
通过直接操作字节码来生成新的类,避免了反射带来的性能开销。 -
启动性能:
CGLIB
在生成代理对象时可能会比JDK
动态代理慢,因为它需要在运行时生成新的字节码。如果代理对象在应用启动时就被创建,这可能会略微影响启动时间。
选择建议
-
如果类已经实现了接口,或者希望强制使用接口编程,那么
JDK
动态代理是一个好选择。 -
如果需要代理没有实现接口的类,或者对性能有较高的要求,特别是在代理方法的调用上,
CGLIB
可能是更好的选择。 -
在现代的
Java
应用中,很多框架(如Spring
)都提供了对这两种代理方式的透明支持,并且可以根据实际情况自动选择使用哪一种。例如,Spring AOP
默认会使用JDK
动态代理,但如果遇到没有实现接口的类,它会退回到CGLIB
。
6. 动态代理的实际应用场景
- 面向切面编程(
AOP
):
-
问题解决:在不改变原有业务逻辑代码的情况下,为程序动态地添加额外的行为(如日志记录、性能监测、事务管理等)。
-
应用实例:
Spring AOP
使用动态代理为方法调用提供了声明式事务管理、安全性检查和日志记录等服务。根据目标对象是否实现接口,Spring AOP
可以选择使用JDK
动态代理或CGLIB
代理。
- 事务管理:
-
问题解决:自动化处理数据库事务的边界,如开始、提交或回滚事务。
-
应用实例:
Spring
框架中的声明式事务管理使用代理技术拦截那些被@Transactional
注解标记的类或方法,确保方法执行在正确的事务管理下进行。
- 权限控制和安全性:
-
问题解决:在执行敏感操作之前自动检查用户权限,确保只有拥有足够权限的用户才能执行某些操作。
-
应用实例:企业应用中,使用代理技术拦截用户的请求,进行权限验证后才允许访问特定的服务或执行操作。
- 延迟加载:
-
问题解决:对象的某些属性可能加载成本较高,通过代理技术,可以在实际使用这些属性时才进行加载。
-
应用实例:
Hibernate
和其他ORM
框架使用代理技术实现了延迟加载(懒加载),以提高应用程序的性能和资源利用率。
- 服务接口调用的拦截和增强:
-
问题解决:对第三方库或已有服务进行包装,添加额外的逻辑,如缓存结果、参数校验等。
-
应用实例:在微服务架构中,可以使用代理技术对服务客户端进行增强,实现如重试、熔断、限流等逻辑。
在现代框架中的应用
-
Spring框架:
Spring
的AOP
模块和事务管理广泛使用了动态代理技术。根据目标对象的类型(是否实现接口),Spring
可以自动选择JDK
动态代理或CGLIB
代理。 -
Hibernate:
Hibernate
使用动态代理技术实现懒加载,代理实体类的关联对象,在实际访问这些对象时才从数据库中加载它们的数据。 -
MyBatis:
MyBatis
框架使用动态代理技术映射接口和SQL
语句,允许开发者通过接口直接与数据库交互,而无需实现类。
欢迎一键三连~
有问题请留言,大家一起探讨学习
----------------------Talk is cheap, show me the code-----------------------
相关文章:
Spring高手之路17——动态代理的艺术与实践
文章目录 1. 背景2. JDK动态代理2.1 定义和演示2.2 不同方法分别代理2.3 熔断限流和日志监控 3. CGLIB动态代理3.1 定义和演示3.2 不同方法分别代理(对比JDK动态代理写法)3.3 熔断限流和日志监控(对比JDK动态代理写法) 4. 动态代理…...
如何在Unity中使用设计模式
在 Unity 环境中,设计模式是游戏开发人员遇到的常见问题的通用解决方案。将它们视为解决游戏开发中特定挑战的经过验证的模板或蓝图。以下是一些简单易懂的设计模式: 1. 单例=> 单例模式确保一个类只有一个实例,并提供对该实例的全局访问点。在 Unity 中,可以使用单例模…...
基于springboot+vue+Mysql的旅游管理系统
开发语言:Java框架:springbootJDK版本:JDK1.8服务器:tomcat7数据库:mysql 5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:…...
vue3+ts中判断输入的值是不是经纬度格式
vue3ts中判断输入的值是不是经纬度格式 vue代码: <template #bdjhwz"{ record }"><a-row :gutter"8" v-show"!record.editable"><a-col :span"12"><a-input placeholder"经度" v-model:v…...
python常用知识总结
文章目录 1. 常用内置函数1. ASCII码与字符相互转换 1. 常用内置函数 1. ASCII码与字符相互转换 # 用户输入字符 c input("请输入一个字符: ")# 用户输入ASCII码,并将输入的数字转为整型 a int(input("请输入一个ASCII码: "))print( c &qu…...
常用的启发式算法
A算法:在电子地图导航软件中,当你输入目的地时,软件就会利用A算法来计算从现在的位置到目的地的最佳路径。该算法兼顾了路径的优化以及计算速度,保证了结果的准确性以及反馈的实时性。 模拟退火算法:模拟退火算法常被…...
应该如何进行POC测试?—【DBA从入门到实践】第三期
在数据库选型过程中,为确保能够灵活应对数据规模的不断扩大和处理需求的日益复杂化,企业和技术人员会借助POC测试来评估不同数据库系统的性能。在测试过程中,性能、并发处理能力、存储成本以及高可用性等核心要素通常会成为大家关注的焦点&am…...
通过Clojure中的集合与序列谈谈抽象的重要
与君共勉:生命不息,学习不止,切忌浮躁,静下心来,每天进步一点点。 Clojure简介 Clojure是一门运行在JVM上面的Lisp方言,其它的Lisp方言还有Scheme、Common Lisp等。Lisp相关的著名书籍有《计算机程序的构…...
Rust---模式(Pattern)匹配
目录 模式是什么它用来做什么模式匹配和赋值为什么会有模式匹配模式匹配用在什么地方match 表达式if let表达式while let表达式for 循环let 语句函数参数不可驳模式匹配和可驳模式匹配模式是什么 在Rust中,模式(Pattern)是一种用于匹配和解构数据的语法结构。模式匹配中常用…...
MATLAB 计算点投影到平面上的坐标(59)
MATLAB 计算点投影到平面上的坐标(59) 一、算法介绍二、算法实现1.代码2.结果一、算法介绍 点投影到平面,计算投影点的坐标,下面提供MATLAB版本的计算程序,直接运行即可,内有验证数据,具体看代码即可。 二、算法实现 1.代码 代码如下(示例): % 平面上的三个点分…...
2024年MathorCup数学建模B题甲骨文智能识别中原始拓片单字自动分割与识别研究解题文档与程序
2024年第十四届MathorCup高校数学建模挑战赛 B题 甲骨文智能识别中原始拓片单字自动分割与识别研究 原题再现: 甲骨文是我国目前已知的最早成熟的文字系统,它是一种刻在龟甲或兽骨上的古老文字。甲骨文具有极其重要的研究价值,不仅对中国文…...
嵌入式与移动物联网开发教程和案例
一、嵌入式与移动物联网概述 嵌入式系统是指嵌入到设备中的专用计算机系统,用于控制、监视或辅助设备操作。而移动物联网则是指通过物联网技术将各种智能设备与互联网连接起来,实现设备之间的互联互通和智能化管理。嵌入式与移动物联网技术的结合&#…...
AttachVoExample
目录 1、 AttachVoExample 1.1、 GeneratedCriteria 1.2、 addCriterion 1.3、 andFnameGreaterThanOrEqualTo 1.4、 GeneratedCriteria Atta...
图像处理特征提取
图像处理中的特征提取是指从图像数据中提取出具有区分性和代表性的特征,以用于图像分类、目标检测、图像匹配等任务。下面介绍几种常见的图像处理特征提取方法: 颜色特征:颜色是图像中最直观且重要的特征之一。常见的颜色特征提取方法包括颜色…...
前端大屏适配几种方案
一、方案一:remfont-size 动态设置HTML根字体大小和body字体大小,会使用到lib-flexible.js插件lib-flexible.js (function flexible(window, document) {var docEl document.documentElementvar dpr window.devicePixelRatio || 1// adjust body font…...
2011年认证杯SPSSPRO杯数学建模B题(第一阶段)生物多样性的评估全过程文档及程序
2011年认证杯SPSSPRO杯数学建模 B题 生物多样性的评估 原题再现: 2010 年是联合国大会确定的国际生物多样性年。保护地球上的生物多样性已经越来越被人类社会所关注,相关的大规模科研和考察计划也层出不穷。为了更好地建立国际交流与专家间的合作&…...
AcWing 793. 高精度乘法——算法基础课题解
AcWing 793. 高精度乘法 题目描述 给定两个非负整数(不含前导 00) A 和 B,请你计算 AB 的值。 输入格式 共两行,第一行包含整数 A,第二行包含整数 B。 输出格式 共一行,包含 AB 的值。 数据范围 1≤…...
【一刷《剑指Offer》】面试题 3:二维数组中的查找
力扣对应题目链接:240. 搜索二维矩阵 II - 力扣(LeetCode) 核心考点:数组相关,特性观察,时间复杂度把握。 一、《剑指Offer》对应内容 二、分析题目 正常查找的过程本质就是排除的过程,谁排除…...
Linux下静态库与动态库使用总结
区别 使用静态库占用的磁盘空间相对比动态库要大。 如果多个可执行程序使用库中同一个函数,那么链接静态库时同一个函数的代码会被复制多份,而链接动态库只复制一份。动态库可共享且版本更新方便 静态链接库在程序编译的时候就被加载进来,不…...
分布式任务调度:架构、原理与实践
引言 在当今快速发展的科技领域中,任务调度作为管理和优化计算资源的重要工具,扮演着至关重要的角色。从单机环境到分布式系统,任务调度的演进不仅跟随着计算机技术的进步,更是为了应对日益复杂的应用场景和需求。本博客将深入探…...
ping命令返回无法访问目标主机和请求超时浅析
在日常经常用ping命令测试网络是否通信正常,使用ping命令时也经常会遇到这两种情况,那么表示网络出现了问题。 1、请求超时的原因 可以看到“请求超时”没有收到任何回复。要知道,IP数据报是有生存时间的,当其生存时间为零时就会…...
地球上的七大洲介绍
地球上的七大洲示意图: 1. 亚洲(Asia):世界上最大的洲,面积约为44579000平方公里。亚洲地域辽阔,包括从北极圈到赤道的各种气候和地形。它拥有世界上最多的人口,也是世界上一些最古老文明的发源…...
IntelliJ IDEA 2024 for Mac/Win:引领Java开发新纪元的高效集成环境
在日新月异的软件开发领域,一款高效、智能的集成开发环境(IDE)无疑是程序员们不可或缺的神兵利器。今天,我要为大家介绍的,正是这样一款集大成之作——IntelliJ IDEA 2024。无论是Mac用户还是Windows用户,只…...
Java 中命令模式,请用代码具体举例
在Java中,命令模式是一种行为设计模式,它允许将请求封装成一个对象,从而使得可以参数化其他对象对请求进行调用、队列化请求、或者记录请求日志,同时支持可撤销的操作。 下面是一个简单的示例代码,展示了如何使用命令模…...
低延时+高并发+强事务丨DolphinDB 交易型内存存储引擎 IMOLTP 使用指南
1. 背景 在一些数据库应用场景中,例如金融行业的交易系统,其主要工作负载来源于对关系表的高频度、高并发的更新和查询操作。这样的应用场景要求数据的读写和计算能够具有低延迟、高并发的特征,同时保证极高的数据一致性,并提供 …...
写代码的修养
看山是山,看水是水 此境界 对业务的思考是浅层的,代码写的不通用,扩展性差,表现在无设计模式 看山不是山,看水不是水 此境界 对业务的思考是中层的,代码写的通用,扩展性好,表现为…...
springboot 问题整合
springboot 启动后访问报错 问题:org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): 原因:mybatis 的全局配置文件和 sql 映射文件没有写 解决:在 application.yml 中添加 mybatis 配置 mybatis:# 全局配…...
UNIAPP二维码展示页亮度调至最亮返回恢复进入前亮度
onLoad(params) {let num plus.screen.getBrightness().toString(); //转字符串是要存到stoage中number类型会存储失败plus.storage.setItem("pmld", num)plus.screen.setBrightness(1); //设置屏幕亮度,范围0-1 }onUnload() {let platformuni.getSystem…...
Golang ProtoBuf 初学者完整教程:安装
一、Protobuf 特点 更高效:使用二进制编码,相比XML/JSON更加高效 跨语言支持:Protobuf 在 .proto 定义需要处理的结构化数据,可以通过 protoc 工具,将 .proto 文件转换为 C、C、Golang、Java、Python 等多种语言的代…...
Isolation Forest 简介
1. 简介 孤立森林 iForest(Isolation Forest)是一种无监督学习算法,用于识别异常值。其基本原理是:异常数据由于数量较少且与正常数据差异较大,因此在被隔离时需要较少的步骤。 两个假设: 1. 异常的值是非常少的(如果异常值很多&…...
博客社区类网站模板下载/网站怎么建立
本节知识点:Ionic之form表单实现MVVM,模拟数据提交或信息注册案例。 效果展示: 【ion-input】单行文本框 <ion-item-divider><ion-label>用户基本信息</ion-label></ion-item-divider><ion-item><ion-labe…...
腾讯邮箱网页版/seo技术服务外包
51%的企业在过去12月内发生过数据泄漏面对日益复杂的安全环境,多样化的攻击手段,传统的防护已经失效,你的安全团队是否做好了准备? 深井式的管理架构,各自封闭的信息系统,无迹可寻的内部泄漏,防…...
旅游景区网站建设/百度关键词推广可以自己做吗
堆排序 堆排序是利用堆的性质进行的一种选择排序。下面先讨论一下堆。 1.堆 堆实际上是一棵完全二叉树,其任何一非叶节点满足性质: Key[i]<key[2i1]&&Key[i]<key[2i2]或者Key[i]>Key[2i1]&&key>key[2i2] 即任何一非叶节点的…...
ui做网站流程/网站提交
PublicClassTestClass Test Private_classid AsString <summary> 设置和获取分类ID </summary>PublicPropertyclassid() GetReturn_classid EndGetSet(ByValvalue) _classid value EndSetEnd PropertyEnd Class...
网站优化我自己可以做吗/google浏览器下载
一、MySQL官网下载MySQL5.7版本,我这里下载的是MySQL5.7.24。二、直接到D:phpStudyPHPTutorial目录下删除之前的MySQL版本,把下载好的MySQL5.7.24版本解压并修改为MySQL,然后在MySQL目录下新建my.ini文件并加入如下内容: [mysqld] port3306 b…...
wap网站开发java/北京网络推广公司
五周第三次课(1月10日)8.1 shell介绍Shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行。实际上Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。8.2 …...