代理模式属于结构型模式,是23种设计模式中较为重要的一种,代理模式分为静态代理和动态代理,动态代理又分为基于接口实现的动态代理和基于子类实现的动态代理;在jdk源码中,很多底层的实现也是基于代理模式的,例如创建线程的方式之一实现Runnable接口就使用了静态代理模式,而Spring框架最为重要的AOP的实现是基于动态代理模式,可见,学习代理模式是我们能看懂底层源码实现原理的一个基础。
静态代理
关于静态代理,我们以小明结婚为例,看以下代码:
//代理对象和真实对象共同实现的接口,里面有结婚方法
interface A{
void marry();
}
//真实对象
class Person implements A{
@Override
public void marry() {
System.out.println("我要结婚啦~~~");
}
}
//代理对象
class Proxy implements A{
//代理类聚合了真实类
private A a;
public Proxy(A a){
this.a = a;
}
@Override
public void marry() {
before();
//真实对象去结婚
a.marry();
after();
}
//增强方法:对真实对象进行一个增强
public void before(){
System.out.println("结婚前,布置现场");
}
public void after(){
System.out.println("结婚后,收拾现场");
}
}
/**
* 使用生活中的结婚例子来理解静态代理:小明要结婚,会去找婚庆公司帮忙准备结婚的一些事宜
* 小明是真实对象,婚庆公司是代理对象,代理了真实对象,帮助小明去结婚,真正结婚的是小明
*
* 静态代理模式通过代理对象可以做很多真实对象做不了的事情,实现对真实对象的一个增强
* 同时真实对象也可以专注的去做一件事情
*
* 就好比婚庆公司可以在小明结婚之前帮他布置现场,结婚之后收拾现场,而小明只需要专心结婚即可
*
* 静态代理模式要求代理对象和真实对象实现同一个接口
* @author cj_chen
*
*/
public class Client {
public static void main(String[] args) {
//创建一个真实对象
Person xiaoming = new Person();
//创建一个代理对象,代理了小明这个真实对象
Proxy proxy = new Proxy(xiaoming);
//通过代理对象去执行真实对象的方法,实现一个增强
proxy.marry();
}
}
关于静态代理模式:
- 要求代理对象和真实对象实现同一个接口
- 一个真实角色就会产生一个代理角色,导致代码量翻倍,开发效率降低
- 静态代理模式通过代理对象可以做很多真实对象做不了的事情,实现对真实对象的一个增强,同时真实对象也可以专注的去做一件事情
静态代理模式在jdk源码中的应用:
实现Runnable接口创建线程的方式用到了静态代理模式;回顾一下步骤:先创建一个Runnable接口的实现类对象,然后创建一个Thread类,并将Runnable接口的实现类作为参数传递进去,然后调用Thread类对象的start方法开启多线程;
Runnable接口的实现类对象是真实对象,Thread类对象是代理对象,静态代理模式要求代理对象和真实对象继承同一个接口,Runnable接口的实现类对象和Thread类对象都继承了Runnable接口,所以完全符合静态代理模式】
动态代理
关于动态代理,我们以生产者生产手机和销售手机为例,看以下代码:
//被代理角色/真实角色实现的接口
public interface IProducer {
//生产手机
void produce();
//销售手机
void sale();
}
//被代理角色
public class Producer implements IProducer{
@Override
public void produce() {
System.out.println("生产者生产手机啦~~~");
}
@Override
public void sale() {
System.out.println("生产者销售手机啦~~~");
}
}
public class Client {
public static void main(String[] args) {
IProducer producer = new Producer();
producer.produce();
}
}
一般来说,客户端可以直接调用真实对象的方法;现在我们有这么一个需求:想要在生产者(真实对象)执行生产方法和销售方法之前打印一句日志,并且是要在不修改原有代码的前提下完成,这也是为了满足设计模式的开闭原则,这时候就可以使用动态代理模式
public class Client {
public static void main(String[] args) {
//先创建一个真实角色
IProducer producer = new Producer();
//基于接口方式创建动态代理角色
IProducer proxy = (IProducer)Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法都会经过该方法
* 方法参数的含义
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//增强真实角色的代码
System.out.println("打印日志lalala~~~");
return method.invoke(producer, args);
}
});
proxy.produce();
}
}
关于动态代理:
- 分类
- 基于接口的动态代理:使用JDK官方给我们提供好的一个Proxy类(java.lang.reflect)来动态的创建代理对象,创建方法是使用Proxy类中的newProxyInstance方法,该方法需要传递三个参数,一是ClassLoader类加载器,用于加载代理对象字节码的,和被代理对象使用相同的类加载器;二是Class[]字节码数组,用于让代理对象和被代理对象有相同方法;三是InvocationHandler,用于提供增强的代码,也就是让我们写如何代理;基于接口的动态代理弊端就是要求被代理类最少实现一个接口,如果没有则不能使用
- 基于子类的动态代理:需要依赖第三方cglib库,如果是maven项目则导入cglib相关的依赖即可,基于子类的动态代理要求被代理类不能是最终类
- 好处:
- 不修改原有代码的基础上对方法进行增强
- 解决了静态代理一个真实对象必须有一个代理对象,导致类数量大大增加的缺点,一个动态代理类可以代理多个类,只要实现的是同一个接口即可
-
PS:动态代理模式底层使用的就是java的反射,要想真正理解动态代理模式,建议先看一下反射相关的知识!
如果对于动态代理模式“一个动态代理类可以代理多个类,只要实现的是同一个接口即可”这个优点不够理解的话,可以看下面这个代码的例子(摘抄自狂神的笔记:https://www.bilibili.com/video/BV1mc411h719?p=11)
代码分析:以上代码其实就是自己写了一个类,这个类包含一个生成代理角色的方法,并且也已经写好了InvocationHandler类的invoke方法,即写好了代码增强的逻辑;这个类还聚合了一个Object类型的接口,根据我们传入的不同真实对象即可动态生成不同代理类。以上代码其实就是一个生成动态代理对象的模版,根据传入不同的target参数,生成不同的代理对象。spring底层创建动态代理对象也有两个模版,JdkDynamicAopProxy和CglibAopProxy,spring容器初始化的时候,如果发现被代理的对象实现了一个接口,那么就会使用基于接口的动态代理,也就是使用JdkDynamicAopProxy这个类来创建代理对象,查看源码可以发现,其实这个类的实现和上面截图手写的模版很像,也是使用Proxy.newProxyInstance方法去创建代理对象。
动态代理的应用:
Spring的AOP(面向切面编程)就运用到了动态代理模式,换言之,AOP的实现方式就是使用动态代理技术,AOP就是在不改变原有代码的基础上,可以将要增强的代码横切进去,例如要给原来的业务逻辑代码增加打印日志或者事务管理等;简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的 基础上,对我们的已有方法进行增强
关于Spring AOP的相关知识,推荐一篇文章:https://www.cnblogs.com/ysocean/p/7476379.html
AOP编程举例:
@RestController
public class DemoController {
@GetMapping("/hello")
public String mytest() {
return "hello,world";
}
@GetMapping("/log")
@Log(desc = "hhh")
public String testLog() {
return "this is log";
}
}
/**
* spring容器启动时,扫描到Aspect注解,知道这是一个切面类,需要进行aop编程
* 然后解析切入点表达式,用切入点表达式和纳入spring容器中的bean做匹配
* 如果匹配成功,则会为该bean创建动态代理对象,并将该动态代理对象纳入spring ioc容器
* 如果匹配不成功,不会创建代理对象,会将原有对象纳入spring ioc容器
* 如果spring为某个进行aop编程的类创建了代理对象,那么通过@Autowired方式得到的就是代理对象
* 调用的方法都是通过代理对象去调用增强后的方法
*/
@Aspect // 代表这是一个切面类
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
// 定义一个切入点,该切入点表达式表示对com.example.springboottest.controller包下的任意类的任意方法进行增强
// 并且不考虑方法的访问修饰符,和方法的参数限制等
@Pointcut("execution(* com.example.springboottest.controller.*.*(..))")
public void myPointcut() {
}
// 该切入点代表对加了Log注解的方法进行增强
@Pointcut("@annotation(com.example.springboottest.annotation.Log))")
public void Pointcut() {
}
// 环绕通知,对符合切入点表达式的方法进行增强
// 对于环绕通知,需要注意两个点:一是方法的参数必须是ProceedingJoinPoint,而是需要用Object接收返回值
@Around("myPointcut()")
public Object myLogger(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
String className = proceedingJoinPoint.getTarget().getClass().toString();
String methodName = proceedingJoinPoint.getSignature().getName();
logger.info("方法执行之前,类名:{}, 方法名:{}", className, methodName);
// 回调被代理对象的方法
Object proceed = proceedingJoinPoint.proceed();
logger.info("方法执行之后,类名:{}, 方法名:{}", className, methodName);
return proceed;
}
@Around("Pointcut()")
public Object logTest(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 获取方法上的注解,并拿到注解的信息
MethodSignature methodSignature = (MethodSignature)proceedingJoinPoint.getSignature();
Method method = methodSignature.getMethod();
String methodName = method.getName();
Log annotation = method.getAnnotation(Log.class);
String desc = annotation.desc();
logger.info("方法执行之前,方法名:{},注解上的信息为:{}", methodName, desc);
// 回调被代理对象的方法
Object proceed = proceedingJoinPoint.proceed();
logger.info("方法执行之前,方法名:{},注解上的信息为:{}", methodName, desc);
return proceed;
}
}
/**
* 自定义一个方法级别的注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String desc() default "";
}
测试mytest方法,后台打印日志为:
测试testLog方法,后台打印日志为: