资讯详情

12-A-代理模式详解

📅 2026/10/18 1:56:28 | 华诺云谱 👁 阅读
12-A-代理模式详解
12-A-代理模式详解关键词代理、静态代理、JDK 动态代理、CGLIB、Spring AOP、MapperProxy、Feign、自调用失效代理模式解决一个问题控制对对象的访问在访问前后加逻辑而调用方无感知。Spring 的事务、日志、权限MyBatis 的 Mapper 接口OpenFeign 的远程调用——全是代理。这是 Java 后端使用频率最高的模式没有之一。 术语速查表术语人话解释代理Proxy和目标对象实现同一接口、持有目标引用、在委托前后加逻辑的对象目标对象Target被代理的真实干活的对象静态代理手写代理类编译期就存在JDK 动态代理运行时生成代理类要求目标类有接口Proxy.newProxyInstanceCGLIB运行时生成目标类的子类来代理无接口也能代理final 类/方法除外自调用失效同类内部this.method()不经过代理AOP 增强不生效一、业务场景给库存服务加访问控制需求InventoryService.deduct()调用前要鉴权 记日志 统计耗时但不想改业务代码。1.1 不用代理横切逻辑侵入业务// ❌ 每个方法都要重复写鉴权/日志/耗时publicbooleandeduct(LongskuId,intcount){longstartSystem.currentTimeMillis();checkPermission();// 横切逻辑log.info(deduct start skuId{},skuId);booleanresultdoDeduct(skuId,count);// 真正的业务log.info(deduct end cost{}ms,System.currentTimeMillis()-start);returnresult;}1.2 静态代理publicinterfaceInventoryService{booleandeduct(LongskuId,intcount);}/** 静态代理手写编译期存在 */publicclassInventoryServiceProxyimplementsInventoryService{privatefinalInventoryServicetarget;publicInventoryServiceProxy(InventoryServicetarget){this.targettarget;}Overridepublicbooleandeduct(LongskuId,intcount){longstartSystem.currentTimeMillis();SecurityContext.checkPermission(inventory:deduct);try{returntarget.deduct(skuId,count);// 委托目标对象}finally{log.info(deduct skuId{} cost{}ms,skuId,System.currentTimeMillis()-start);}}}缺点接口有 10 个方法就要写 10 段重复的包装100 个服务就要写 100 个代理类——所以需要动态代理。1.3 JDK 动态代理publicclassTimingInvocationHandlerimplementsInvocationHandler{privatefinalObjecttarget;publicTimingInvocationHandler(Objecttarget){this.targettarget;}OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{longstartSystem.currentTimeMillis();SecurityContext.checkPermission(method.getName());try{returnmethod.invoke(target,args);}finally{log.info({}.{} cost{}ms,target.getClass().getSimpleName(),method.getName(),System.currentTimeMillis()-start);}}SuppressWarnings(unchecked)publicstaticTTwrap(Ttarget,ClassTiface){return(T)Proxy.newProxyInstance(iface.getClassLoader(),newClass?[]{iface},newTimingInvocationHandler(target));}}// 使用一行包装任意接口InventoryServiceproxiedTimingInvocationHandler.wrap(realService,InventoryService.class);proxied.deduct(1001L,2);// 自动带上鉴权日志耗时限制只能代理接口——Proxy.newProxyInstance生成的代理类继承java.lang.ProxyJava 单继承决定了它只能再实现接口。1.4 CGLIB无接口的类也能代理publicclassTimingMethodInterceptorimplementsMethodInterceptor{OverridepublicObjectintercept(Objectobj,Methodmethod,Object[]args,MethodProxyproxy)throwsThrowable{longstartSystem.currentTimeMillis();try{returnproxy.invokeSuper(obj,args);// 调用父类目标类方法}finally{log.info({} cost{}ms,method.getName(),System.currentTimeMillis()-start);}}}// CGLIB 生成目标类的子类作为代理EnhancerenhancernewEnhancer();enhancer.setSuperclass(InventoryServiceImpl.class);// 不需要接口enhancer.setCallback(newTimingMethodInterceptor());InventoryServiceImplproxied(InventoryServiceImpl)enhancer.create();限制final类不能被继承、final/private/static方法不能被覆写——都代理不了。1.5 Spring AOP生产里的正确姿势AspectComponentpublicclassTimingAspect{Around(execution(* com.yc.corp.ceo..service..*.*(..)))publicObjecttiming(ProceedingJoinPointpjp)throwsThrowable{longstartSystem.currentTimeMillis();try{returnpjp.proceed();}finally{log.info({}.{} cost{}ms,pjp.getTarget().getClass().getSimpleName(),pjp.getSignature().getName(),System.currentTimeMillis()-start);}}}Spring AOP 的选择规则目标类有接口 → JDK 动态代理无接口 → CGLIBSpring Boot 2.x 默认全用 CGLIBspring.aop.proxy-target-classtrue。Transactional、Async、Cacheable的底层全是这套代理机制。下面是代理模式的整体调用流程是否客户端 Client代理对象 Proxy是否需要额外处理前置增强权限校验 / 日志 / 耗时统计调用真实对象RealSubject后置增强结果处理 / 日志返回结果给客户端从图中可以看出客户端始终只与代理对象交互代理对象在调用真实对象前后可以插入各种增强逻辑这正是代理模式的核心价值所在。二、三种代理方式对比维度静态代理JDK 动态代理CGLIB代理类生成时机编译期手写运行时反射生成运行时生成子类字节码要求无目标必须有接口目标不能是 final性能现代 JVM最快创建慢、调用快创建慢、调用快典型应用简单包装Feign、MyBatis MapperProxySpring AOP 默认三、框架中的同款实现框架应用说明SpringJdkDynamicAopProxy/ObjenesisCglibAopProxyTransactional/Async/自定义切面全靠它MyBatisMapperProxyMapper 接口没有实现类却能调用——JDK 动态代理把方法调用转成 SQL 执行OpenFeignReflectiveFeign.FeignInvocationHandler接口方法调用 → HTTP 请求纯代理模式Hibernate懒加载实体代理HibernateProxyget 时才查库延迟加载RPC 框架DubboInvoker代理本地方法调用 → 远程网络调用JDKjava.lang.reflect.Proxy动态代理的基础设施MyBatis MapperProxy 值得细看——它解释了为什么只写接口就能用// Mapper 接口方法被调用时实际执行的是publicObjectinvoke(Objectproxy,Methodmethod,Object[]args){// (1) Object 方法toString 等直接放行// (2) 接口默认方法走 DefaultMethodInvoker// (3) 其余方法缓存 MapperMethod执行 sqlSession.selectOne/insert/...returncachedInvoker(method).invoke(proxy,method,args,sqlSession);}四、代理 vs 装饰器 vs 适配器第三次辨析这次从生成方式看模式接口意图代理对象谁创建代理不变控制访问鉴权/延迟/远程/事务通常框架自动创建调用方不知道拿的是代理装饰器不变加功能可多层组合调用方显式组装见 09-A适配器改变接口转换调用方显式创建见 06-A五、常见误用与生产事故事故一同类自调用事务失效ServicepublicclassOrderService{publicvoidbatchCreate(ListOrderorders){for(Ordero:orders){this.createOne(o);// ❌ this 是原始对象不是代理——Transactional 不生效}}TransactionalpublicvoidcreateOne(Orderorder){orderDao.insert(order);pointDao.add(order.getUserId(),10);}}原因Spring 注入的是代理对象但this.createOne()走的是原始对象的方法引用绕过了代理事务增强根本没执行。批量插入中途失败已插入的订单不会回滚。修复三选拆类createOne挪到另一个 Bean通过注入调用推荐顺便解决职责问题自注入Lazy Autowired private OrderService self;然后self.createOne(o)AopContext.currentProxy()要开exposeProxytrue不推荐隐式依赖。同款失效场景还有private方法CGLIB 无法覆写、final方法、非 public 方法上的Transactional——完整清单见 07-01-B-Spring与SpringBoot面试与生产事故实战。事故二代理对象上取不到目标类的注解// ❌ 拿到的是 CGLIB 代理类getClass() 返回 OrderService$$SpringCGLIB$$0Annotationannservice.getClass().getAnnotation(MyAnno.class);// null修复用 Spring 的工具类穿透代理Class?targetClassAopUtils.getTargetClass(service);// 真实类MyAnnoannAnnotatedElementUtils.findMergedAnnotation(targetClass,MyAnno.class);误用给纯内存计算的工具类上 AOP切面匹配范围写成execution(* com.yc..*.*(..))把 DTO 的 getter、工具类的静态计算全代理了——启动变慢、调用栈变深、排查问题时堆栈里全是$$EnhancerBySpringCGLIB$$。切点表达式要精确到确实需要增强的服务层方法。六、总结代理的核心接口不变、控制访问、调用方无感——事务/鉴权/日志/远程/懒加载的统一实现手段。三种形态静态代理手写简单场景、JDK 动态代理要接口Feign/MapperProxy 在用、CGLIB生成子类Spring AOP 默认。头号大坑是自调用失效this.method()绕过代理Transactional/Async/Cacheable全部失灵——拆类或自注入修复。final/private/static 方法代理不了CGLIB 靠继承覆写写服务类时留意。判断拿到的是不是代理AopUtils.isAopProxy(bean)取真实类用AopUtils.getTargetClass()。上一篇11-A-享元模式详解下一篇13-A-责任链模式详解
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑