上一章通过@AspectJ
和基于schema
的切面定义形容了Spring对AOP的反对。在本章中,咱们探讨了较低级别的Spring AOP API。对于常见的应用程序,咱们倡议将Spring AOP与AspectJ切入点一起应用,如上一章所述。
6.1 本节形容了Spring如何解决要害切入点概念。
6.1.1 概念
Spring的切入点模型使切入点重用不受告诉类型的影响。你能够应用雷同的切入点来定位不同的告诉。org.springframework.aop.Pointcut
接口是外围接口,用于将告诉定向到特定的类和办法。残缺的接口如下:
public interface Pointcut { ClassFilter getClassFilter(); MethodMatcher getMethodMatcher();}
将Pointcut
接口分为两局部,能够重用类和办法匹配的局部以及细粒度的合成操作(例如与另一个办法匹配器执行“联结”)。
ClassFilter
接口用于将切入点限度为给定的一组指标类。如果matches()
办法始终返回true
,则将匹配所有指标类。以下清单显示了ClassFilter
接口定义:
public interface ClassFilter { boolean matches(Class clazz);}
MethodMatcher
接口通常更重要。残缺的接口如下:
public interface MethodMatcher { boolean matches(Method m, Class targetClass); boolean isRuntime(); boolean matches(Method m, Class targetClass, Object[] args);}
matchs(Method,Class)
办法用于测试此切入点是否与指标类上的给定办法匹配。创立AOP代理时能够执行此评估,以防止须要对每个办法调用进行测试。如果两个参数的match
办法对于给定的办法返回true
,并且MethodMatcher
的isRuntime()
办法返回true
,则在每次办法调用时都将调用三个参数的match
办法。这样,切入点就能够在执行指标告诉之前立刻查看传递给办法调用的参数。
大多数MethodMatcher
实现都是动态的,这意味着它们的isRuntime()
办法返回false
。在这种状况下,永远不会调用三参数匹配办法。
如果可能,请尝试使切入点成为动态,以容许AOP框架在创立AOP代理时缓存切入点评估的后果。
6.1.2 切入点的操作
Spring反对切入点上的操作(特地是联结和交加)。
联结示意两个切入点匹配其中一个的办法。交加是指两个切入点都匹配的办法。联结通常更有用。你能够通过应用org.springframework.aop.support.Pointcuts
类中的静态方法或应用同一包中的ComposablePointcut
类来组成切入点。然而,应用AspectJ切入点表达式通常是一种更简略的办法。然而,应用AspectJ切入点表达式通常是一种更简略的办法。
6.1.3 AspectJ 表达式切入点
从2.0开始,Spring应用的最重要的切入点类型是org.springframework.aop.aspectj.AspectJExpressionPointcut
。这是一个切入点,该切入点应用AspectJ提供的库来解析AspectJ切入点表达式字符串。
无关反对的AspectJ切入点原语的探讨,请参见上一章。
6.1.4 便捷切入点实现
Spring提供了几种不便的切入点实现。你能够间接应用其中一些。其余的则打算在特定于应用程序的切入点中被子类化。
动态切入点
动态切入点基于办法和指标类,并且不能思考办法的参数。动态切入点足以满足大多数用处,并且最好。首次调用办法时,Spring只能评估一次动态切入点。之后,无需在每次办法调用时再次评估切入点(备注:第一次评估后进行缓存)。
本节的其余部分形容了Spring附带的一些动态切入点实现。
正则表达式切入点
指定动态切入点的一种显著办法是正则表达式。除了Spring之外,还有几个AOP框架使之成为可能。org.springframework.aop.support.JdkRegexpMethodPointcut
是一个通用的正则表达式切入点它应用JDK中的正则表达式反对。
应用JdkRegexpMethodPointcut
类,能够提供模式字符串的列表。如果其中任何一个匹配,则切入点的评估后果为true
。(因而,后果实际上是这些切入点的并集。)
以下示例显示如何应用JdkRegexpMethodPointcut
:
<bean id="settersAndAbsquatulatePointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut"> <property name="patterns"> <list> <value>.*set.*</value> <value>.*absquatulate</value> </list> </property></bean>
Spring提供了一个名为RegexpMethodPointcutAdvisor
的便捷类,该类使咱们还能够援用一个Advice
(请记住,Advice
能够是拦截器、前置告诉、异样告诉等)。在幕后,Spring应用了JdkRegexpMethodPointcut
。应用RegexpMethodPointcutAdvisor
简化了连贯,因为一个bean同时封装了切入点和告诉,如上面的示例所示:
<bean id="settersAndAbsquatulateAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice"> <ref bean="beanNameOfAopAllianceInterceptor"/> </property> <property name="patterns"> <list> <value>.*set.*</value> <value>.*absquatulate</value> </list> </property></bean>
你能够将RegexpMethodPointcutAdvisor
与任何Advice
类型一起应用。
属性驱动的切入点
动态切入点的一种重要类型是元数据驱动的切入点。这将应用元数据属性的值(通常是源级别的元数据)。
动静切入点
动静切入点比动态切入点更低廉。它们思考了办法参数以及动态信息。这意味着必须在每次办法调用时对它们进行评估,并且因为参数会有所不同,因而无奈缓存后果。
次要示例是control flow
切入点。
控制流切入点
Spring控制流切入点在概念上相似于AspectJ cflow
切入点,只管性能不那么弱小。(目前还没有方法指定一个切入点在与另一个切入点匹配的连接点上面执行。)控制流切入点与以后调用堆栈匹配。例如,如果连接点是由com.mycompany.web
包中的办法或SomeCaller
类调用的,则可能会触发。通过应用org.springframework.aop.support.ControlFlowPointcut
类指定控制流切入点。通过应用org.springframework.aop.support.ControlFlowPointcut
类指定控制流切入点。
与其余动静切入点相比,控制流切入点在运行时进行评估要低廉得多。在Java 1.4中,老本大概是其余动静切入点的五倍。
6.1.5 切入点超类
Spring提供了有用的切入点超类,以帮忙你实现本人的切入点。因为动态切入点最有用,所以你可能应该子类化StaticMethodMatcherPointcut
。这仅须要实现一个形象办法(只管你能够笼罩其余办法以自定义行为)。上面的示例显示如何对StaticMethodMatcherPointcut
进行子类化:
class TestStaticPointcut extends StaticMethodMatcherPointcut { public boolean matches(Method m, Class targetClass) { // return true if custom criteria match }}
动静切入点也有超类。你能够将自定义切入点与任何告诉类型一起应用。
6.1.6 自定义切面
因为Spring AOP中的切入点是Java类,而不是语言性能(如AspectJ),所以你能够申明自定义切入点,无论是动态还是动静。Spring中的自定义切入点能够任意简单。然而,如果能够的话,咱们倡议应用AspectJ切入点表白语言。
更高版本的Spring可能提供对JAC提供的“语义切入点”的反对,例如“所有更改指标对象中实例变量的办法”。
6.2 Spring中的告诉API
当初,咱们能够查看Spring AOP如何解决告诉。
6.2.1 告诉生命周期
每个告诉都是一个Spring bean。告诉实例能够在所有告诉对象之间共享,或者对于每个告诉对象都是惟一的。这对应于每个类或每个实例的告诉。
每个类告诉最罕用。实用于个别告诉,例如事物advisors
(切面和告诉组合)。这些不依赖于代理对象的状态或增加新状态。它们仅作用于办法和参数。
每个实例的告诉都适宜引入,以反对mixins
。在这种状况下,告诉将状态增加到代理对象。
你能够在同一AOP代理中混合应用共享告诉和基于实例的告诉。
6.2.2 Spring中告诉类型
Spring提供了几种告诉类型,并且能够扩大以反对任意告诉类型。本节介绍基本概念和规范告诉类型。
拦挡盘绕告诉
Spring中最根本的告诉类型是盘绕告诉的拦挡。
对于应用办法拦挡的告诉,Spring合乎AOP Alliance接口。实现MethodInterceptor
和盘绕告诉的类也应该实现以下接口:
public interface MethodInterceptor extends Interceptor { Object invoke(MethodInvocation invocation) throws Throwable;}
invoke()
办法的MethodInvocation
参数公开了被调用的办法、指标连接点、AOP代理和办法的参数。invoke()
办法应返回调用的后果:连接点的返回值。
以下示例显示了一个简略的MethodInterceptor
实现:
public class DebugInterceptor implements MethodInterceptor { public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("Before: invocation=[" + invocation + "]"); Object rval = invocation.proceed(); System.out.println("Invocation returned"); return rval; }}
请留神对MethodInvocation
的proceed()
办法的调用。这沿着拦截器链向下达到连接点。大多数拦截器都调用此办法并返回其返回值。然而,MethodInterceptor
就像其余的盘绕告诉一样,能够返回不同的值或引发异样,而不是调用proceed
办法。然而,你没有充沛的理由就不要这样做。
MethodInterceptor
实现提供与其余合乎AOP Alliance要求的AOP实现的互操作性。本节其余部分探讨的其余告诉类型将实现常见的AOP概念,但以特定于Spring的形式。只管应用最具体的告诉类型有一个劣势,然而如果你可能想在另一个AOP框架中运行切面,则在盘绕告诉应用MethodInterceptor
。请留神,切入点以后无奈在框架之间互操作,并且AOP Alliance以后未定义切入点接口。
前置告诉
一种最简略的告诉类型是前置告诉。这个不须要MethodInvocation
对象,因为它仅仅在进入办法前被调用。
前置告诉的次要长处在于,无需调用proceed()
办法,因而,不会因忽略而未能沿拦截器链继续前进。
以下清单显示了MethodBeforeAdvice
接口:
public interface MethodBeforeAdvice extends BeforeAdvice { void before(Method m, Object[] args, Object target) throws Throwable;}
(只管通常的对象实用于字段拦挡,并且Spring不太可能实现它,但Spring的API设计容许前置告诉。)
请留神,返回类型为void
。告诉能够在连接点执行之前插入自定义行为,但不能更改返回值。如果前置的告诉引发异样,它将停止拦截器链的进一步执行。异样会流传回拦截器链。如果是未查看异样在调用的办法的签名上,则将其间接传递给客户端。否则,它将被AOP代理包装在未经查看的异样中。
以下示例显示了Spring中的before
告诉,该告诉计算所有办法调用:
public class CountingBeforeAdvice implements MethodBeforeAdvice { private int count; public void before(Method m, Object[] args, Object target) throws Throwable { ++count; } public int getCount() { return count; }}
前置告诉能够被应用于任何切入点。
异样告诉
如果连接点引发异样,则在连接点返回后调用引发告诉。Spring提供抛出异样告诉。留神这意味着org.springframework.aop.ThrowsAdvice
接口不蕴含任何办法。这是一个标记接口,示意这个对象实现一个或多个抛出异样告诉办法。这些应采纳以下模式:
afterThrowing([Method, args, target], subclassOfThrowable)
仅最初一个参数是必须的。办法签名能够具备一个或四个参数,具体取决于告诉办法是否对该办法参数感兴趣。接下来的两个清单显示类,它们是抛出异样告诉的示例。
如果引发RemoteException
(包含从子类),则调用以下告诉:
public class RemoteThrowsAdvice implements ThrowsAdvice { public void afterThrowing(RemoteException ex) throws Throwable { // Do something with remote exception }}
与后面的告诉不同,下一个示例申明了四个参数,以便能够拜访被调用的办法、办法参数和指标对象。如果抛出ServletException
,则调用以下告诉:
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice { public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) { // Do something with all arguments }}
最初一个示例阐明如何在解决RemoteException
和ServletException
的单个类中应用这两种办法。能够将任意数量的异样告诉办法组合到一个类中。以下清单显示了最初一个示例:
public static class CombinedThrowsAdvice implements ThrowsAdvice { public void afterThrowing(RemoteException ex) throws Throwable { // Do something with remote exception } public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) { // Do something with all arguments }}
如果throws-advice
办法自身引发异样,则它将笼罩原始异样(也就是说,它将更改引发给用户的异样)。重写异样通常是RuntimeException
,它与任何办法签名都兼容。然而,如果throws-advice
办法抛出一个已查看的异样,则它必须与指标办法的申明异样匹配,因而在某种程度上与特定的指标办法签名耦合。不要抛出与指标办法签名不兼容的未声明的查看异样!异样告诉能够被应用与任何切入点。
后置返回告诉
在Spring中,后置返回告诉必须实现org.springframework.aop.AfterReturningAdvice
接口,以下清单显示:
public interface AfterReturningAdvice extends Advice { void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable;}
后置返回告诉能够拜访返回值(无奈批改),调用的办法、办法的参数和指标。
上面的后置返回告诉内容将计数所有未引发异样的胜利办法调用:
public class CountingAfterReturningAdvice implements AfterReturningAdvice { private int count; public void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable { ++count; } public int getCount() { return count; }}
此告诉不会更改执行门路。如果它抛出异样,则抛出的是拦截器链,而不是返回值。
后置返回告诉能够被用于任何切入点。
引入告诉
Spring将引入告诉视为一种非凡的拦挡告诉。
引入须要实现以下接口的IntroductionAdvisor
和IntroductionInterceptor
:
public interface IntroductionInterceptor extends MethodInterceptor { boolean implementsInterface(Class intf);}
从AOP Alliance MethodInterceptor
接口继承的invoke()
办法必须实现引入。也就是说,如果被调用的办法在引入的接口上,则引入拦截器负责解决办法调用,不能调用proceed()
。
引入告诉不能与任何切入点一起应用,因为它仅实用于类,而不适用于办法级别。你只能通过IntroductionAdvisor
应用引入告诉,它具备以下办法:
public interface IntroductionAdvisor extends Advisor, IntroductionInfo { ClassFilter getClassFilter(); void validateInterfaces() throws IllegalArgumentException;}public interface IntroductionInfo { Class<?>[] getInterfaces();}
没有MethodMatcher
,因而没有与引入告诉相干的Pointcut
。只有类过滤是合乎逻辑的。
getInterfaces()
办法返回此advisor
引入的接口。
在外部应用validateInterfaces()
办法来查看引入的接口是否能够由配置的IntroductionInterceptor
实现。
考虑一下Spring测试套件中的一个示例,并假如咱们想为一个或多个对象引入以下接口:
public interface Lockable { void lock(); void unlock(); boolean locked();}
这阐明了混合。咱们心愿可能将告诉对象强制转换为Lockable
,无论它们的类型和调用锁和解锁办法如何。如果咱们调用lock()
办法,咱们心愿所有的setter
办法都抛出一个LockedException
。因而,咱们能够增加一个切面,使对象在不理解对象的状况下不可变:AOP的一个很好的例子。
首先,咱们须要一个IntroductionInterceptor
来实现沉重的工作。在这种状况下,咱们扩大了org.springframework.aop.support.DelegatingIntroductionInterceptor
便当类。咱们能够间接实现IntroductionInterceptor
,然而在大多数状况下,最好应用DelegatingIntroductionInterceptor
。
DelegatingIntroductionInterceptor
被设计为将引入委托给所引入接口的理论实现,而暗藏了监听的应用。你能够应用结构函数参数将委托设置为任何对象。默认委托(应用无参数构造函数时)是this
。因而,在下一个示例中,委托是DelegatingIntroductionInterceptor
的LockMixin
子类。给定一个委托(默认状况下为自身),DelegatingIntroductionInterceptor
实例将查找由委托实现的所有接口(IntroductionInterceptor
除外),并反对针对其中任何一个的引入。诸如LockMixin
的子类能够调用suppressInterface(Class intf)
办法来禁止不应公开的接口。然而,无论IntroductionInterceptor
筹备反对多少个接口,IntroductionAdvisor
被应用管制理论公开哪些接口。引入的接口暗藏了指标对同一接口的任何实现。
因而,LockMixin
扩大了DelegatingIntroductionInterceptor
并实现了Lockable
自身。超类会主动抉择能够反对Lockable
进行引入的办法,因而咱们不须要指定它。咱们能够通过这种形式引入任意数量的接口。
留神locked
实例变量的应用。这无效地将附加状态增加到指标对象中保留。
上面的示例显示LockMixin
类:
public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable { private boolean locked; public void lock() { this.locked = true; } public void unlock() { this.locked = false; } public boolean locked() { return this.locked; } public Object invoke(MethodInvocation invocation) throws Throwable { if (locked() && invocation.getMethod().getName().indexOf("set") == 0) { throw new LockedException(); } return super.invoke(invocation); }}
通常,你无需重写invoke()
办法。通常足以满足DelegatingIntroductionInterceptor
实现(如果引入了办法,则调用委托办法,否则进行到连接点)。在当前情况下,咱们须要增加一个查看:如果处于锁定模式,则不能调用任何setter
办法。
所需的引入只须要保留一个不同的LockMixin
实例并指定引入的接口(在本例中,仅为Lockable
)。一个更简单的示例可能援用了引入拦截器(将被定义为原型)。在这种状况下,没有与LockMixin
相干的配置,因而咱们应用new
创立它。以下示例显示了咱们的LockMixinAdvisor
类:
public class LockMixinAdvisor extends DefaultIntroductionAdvisor { public LockMixinAdvisor() { super(new LockMixin(), Lockable.class); }}
咱们能够非常简单地利用此advisor
程序,因为它不须要配置。(然而,如果没有IntroductionAdvisor
,则无奈应用IntroductionInterceptor
。)像通常的介绍一样,advisor
必须是按实例的,因为它是有状态的。对于每个被告诉的对象,咱们须要一个LockMixinAdvisor
的不同实例,因而也须要LockMixin
的不同实例。advisor
蕴含被告诉对象状态的一部分。咱们能够像其余任何advisor
一样,通过应用Advised.addAdvisor()
办法或XML配置(举荐形式)以编程形式利用此advisor
。下文探讨的所有代理创立抉择,包含“主动代理创立器”,都能够正确处理引入和有状态的混合。
6.3 在Spring中的Advisor API
在Spring中,Advisor
是只蕴含一个与切入点表达式关联的告诉对象的切面。
除了介绍的非凡状况外,任何advisor
都能够与任何告诉一起应用。org.springframework.aop.support.DefaultPointcutAdvisor
是最罕用的advisor
类。它能够与MethodInterceptor
,BeforeAdvice
或ThrowsAdvice
一起应用。
能够在Spring中将advisor
和advice
类型混合在同一个AOP代理中。在一个代理配置中,能够应用盘绕告诉、异样告诉和前置告诉的拦挡。Spring主动创立必要的拦截器链。
6.4 应用ProxyFactoryBean
创立AOP代理
如果你的业务对象应用Spring IoC容器(一个ApplicationContext
或BeanFactory
)(你应该这样做!),那么你想应用Spring的AOP FactoryBean
实现之一。(请记住,工厂bean引入了一个间接层,使它能够创立其余类型的对象。)
Spring AOP反对还在后盾应用了工厂bean。
在Spring中创立AOP代理的根本办法是应用org.springframework.aop.framework.ProxyFactoryBean
。这样能够齐全管制切入点,任何实用的告诉及其程序。然而,如果不须要这样的管制,则有一些更简略的选项比拟可取。
6.4.1 根底
像其余Spring FactoryBean
实现一样,ProxyFactoryBean
引入了一个间接级别。如果定义一个名为foo
的ProxyFactoryBean
,则援用foo
的对象将看不到ProxyFactoryBean
实例自身,而是看到由ProxyFactoryBean
中的getObject()
办法的实现创立的对象。此办法创立一个包装指标对象的AOP代理。
应用ProxyFactoryBean
或另一个反对IoC的类创立AOP代理的最重要益处之一是告诉和切入点也能够由IoC治理。这是一项弱小的性能,能够实现某些其余AOP框架难以实现的办法。例如,告诉自身能够援用应用程序对象(除了指标对象,它应该在任何AOP框架中都可用),这得益于依赖注入提供的所有可插入性。
6.4.2 JavaBean属性
与Spring提供的大多数FactoryBean
实现一样,ProxyFactoryBean
类自身也是一个JavaBean。其属性用于:
- 指定要代理的指标。
- 指定是否应用CGLIB(稍后介绍,另请参见基于JDK和CGLIB的代理)。
一些要害属性继承自org.springframework.aop.framework.ProxyConfig
(Spring中所有AOP代理工厂的超类)。这些要害属性包含:
proxyTargetClass
:如果要代替指标类而不是指标类的接口,则为true
。如果此属性值设置为true
,则将创立CGLIB代理(另请参见基于JDK和CGLIB的代理)。optimize
: 管制被动优化是否利用于通过CGLIB创立的代理。除非你齐全理解相干的AOP代理如何解决优化,否则不要随便应用此设置。以后仅用于CGLIB代理。它对JDK动静代理有效。frozen
: 如果代理配置被解冻,则不再容许对配置进行更改。当你不心愿调用者在创立代理后(通过已告诉接口)可能操作代理时,这对于轻微的优化是十分有用的。此属性的默认值为false
,因而容许进行更改(例如增加其余告诉)。exposeProxy
:确定以后代理是否应该在ThreadLocal
中裸露,以便指标能够拜访它。如果指标须要获取代理,并且裸露代理属性设置为true
,则指标能够应用AopContext.currentProxy()
办法。
ProxyFactoryBean
特有的其余属性包含:
proxyInterfaces
:字符串接口名称的数组。如果未提供,则应用指标类的CGLIB代理(另请参见基于JDK和CGLIB的代理)。interceptorNames
:Advisor
,拦截器或要利用的其余告诉名称的字符串数组。程序很重要,先到先得。也就是说,列表中的第一个拦截器是第一个可能拦挡调用的拦截器。- 你能够在拦截器名称后加上星号(
*
)。这样做会导致所有advisor
bean的应用程序的名称都以要利用的星号之前的局部结尾。你能够在应用Global Advisors中找到应用此个性的示例。 singleton
:无论getObject()
办法被调用的频率如何,工厂是否应返回单个对象。一些FactoryBean
实现提供了这种办法。默认值是true
。如果你想应用有状态告诉—例如,有状态混合—应用原型告诉和单例值false
。
6.4.3 基于JDK和CGLIB代理
本局部是无关ProxyFactoryBean
如何抉择为特定指标对象(将被代理)创立基于JDK的代理或基于CGLIB的代理的权威性文档。
在Spring的1.2.x版和2.0版之间,ProxyFactoryBean
的行为与创立基于JDK或CGLIB的代理无关。当初,ProxyFactoryBean
在自动检测接口方面展现了与TransactionProxyFactoryBean
类相似的语义。
如果要代理的指标对象的类(以下简称为指标类)没有实现任何接口,则创立基于CGLIB的代理。这是最简略的状况,因为JDK代理是基于接口的,并且没有接口意味着甚至无奈进行JDK代理。你能够插入指标bean并通过设置interceptorNames
属性来指定拦截器列表。请留神,即便ProxyFactoryBean
的proxyTargetClass
属性已设置为false
,也会创立基于CGLIB的代理。(这样做没有任何意义,最好将其从bean定义中删除,因为它充其量是多余,并且在最糟的状况下会造成混同。)
如果指标类实现一个(或多个)接口,则创立的代理类型取决于ProxyFactoryBean
的配置。
如果ProxyFactoryBean
的proxyTargetClass
属性已设置为true
,则将创立基于CGLIB的代理。这很有情理,也合乎最小意外准则。即便ProxyFactoryBean
的proxyInterfaces
属性被设置为一个或多个齐全限定的接口名,proxyTargetClass
属性被设置为true
也会使基于cglib的代理失效。
如果ProxyFactoryBean
的proxyInterfaces
属性被设置为一个或多个齐全限定的接口名称,那么将创立一个基于jdk的代理。创立的代理实现了proxyInterfaces
属性中指定的所有接口。如果指标类碰巧实现了比proxyInterfaces
属性中指定的更多的接口,那也没什么问题,然而那些额定的接口不是由返回的代理实现的。
如果没有设置ProxyFactoryBean
的proxyInterfaces
属性,然而指标类实现了一个(或多个)接口,那么ProxyFactoryBean
会自动检测到指标类的确实现了至多一个接口,并创立一个基于jdk的代理。理论代理的接口是指标类实现的所有接口。实际上,这与将指标类实现的每个接口的列表提供给proxyInterfaces
属性雷同。然而,这大大减少了工作量,也不容易呈现书写谬误。
6.4.4 代理接口
思考一个简略的ProxyFactoryBean
操作示例。此示例波及:
- 代理的指标bean。这是示例中的
personTarget
bean定义。 - 用于提供告诉的
Advisor
和拦截器。 - 一个用于指定指标对象(
personTarget
bean)、代理接口和利用告诉的AOP代理bean定义。
以下清单显示了示例:
<bean id="personTarget" class="com.mycompany.PersonImpl"> <property name="name" value="Tony"/> <property name="age" value="51"/></bean><bean id="myAdvisor" class="com.mycompany.MyAdvisor"> <property name="someProperty" value="Custom string property value"/></bean><bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"></bean><bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces" value="com.mycompany.Person"/> <property name="target" ref="personTarget"/> <property name="interceptorNames"> <list> <value>myAdvisor</value> <value>debugInterceptor</value> </list> </property></bean>
留神,interceptorNames
属性承受一个字符串列表,其中蕴含以后工厂中的拦截器或advisors
的bean名称。你能够应用advisors
、拦截器、前置告诉、后置告诉和异样告诉对象。advisors
的程序很重要。
你可能想晓得为什么列表不保留bean援用。这样做的起因是,如果ProxyFactoryBean
的singleton
属性被设置为false
,那么它必须可能返回独立的代理实例。如果任何advisors
自身是原型,则须要返回一个独立的实例,因而必须可能从工厂取得原型的实例。
能够应用后面显示的person
Bean定义代替Person
实现,如下所示:
Person person = (Person) factory.getBean("person");
与一般Java对象一样,在同一IoC上下文中的其余bean能够表白对此的强类型依赖性。以下示例显示了如何执行此操作:
<bean id="personUser" class="com.mycompany.PersonUser"> <property name="person"><ref bean="person"/></property></bean>
在此示例中,PersonUser
类裸露了Person
类型的属性。就其自身而言,AOP代理能够通明地代替真person
实现。然而,其类将是动静代理类。能够将其转换为Advised
接口(稍后探讨)。
你能够应用匿名外部bean暗藏指标和代理之间的区别。仅ProxyFactoryBean
定义不同。该倡议仅出于完整性思考。以下示例显示了如何应用匿名外部Bean:
<bean id="myAdvisor" class="com.mycompany.MyAdvisor"> <property name="someProperty" value="Custom string property value"/></bean><bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/><bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces" value="com.mycompany.Person"/> <!-- Use inner bean, not local reference to target --> <property name="target"> <bean class="com.mycompany.PersonImpl"> <property name="name" value="Tony"/> <property name="age" value="51"/> </bean> </property> <property name="interceptorNames"> <list> <value>myAdvisor</value> <value>debugInterceptor</value> </list> </property></bean>
应用匿名外部bean的长处是只有一个Person
类型的对象。如果咱们想避免应用程序上下文的用户取得对未告诉对象的援用,或者须要防止应用Spring IoC主动拆卸产生歧义,这是很有用的。能够说,还有一个长处是ProxyFactoryBean
定义是独立的。然而,有时候,可能从工厂取得未告诉的指标实际上可能是一种劣势(例如,在某些测试场景中)。
6.4.5 代理类
如果须要代理一个类,而不是一个或多个接口怎么办?
设想一下,在咱们之前的示例中,没有Person
接口。咱们须要告诉一个名为Person
的类,该类没有实现任何业务接口。在这种状况下,你能够将Spring配置为应用CGLIB代理而不是动静代理。为此,请将后面显示的ProxyFactoryBean
的proxyTargetClass
属性设置为true
。尽管最好是依据接口而不是类编程,但在解决遗留代码时,告诉没有实现接口的类的能力可能很有用。(一般来说,Spring是没有规定性的。尽管它使利用良好实际变得容易,但它防止了强制应用特定的形式或办法。)
如果须要,即便有接口,也能够在任何状况下强制应用CGLIB。
CGLIB代理通过在运行时生成指标类的<u>子类</u>来工作。Spring配置此生成的子类以将办法调用委托给原始指标。子类用于实现Decorator
模式,并编织在告诉中。
CGLIB代理通常应答用户通明。然而,有一些问题要思考:
final
的办法不能被告诉,因为它们不能被笼罩(备注:子类不能笼罩被final
标记办法)。- 无需将CGLIB增加到你的类门路中。从Spring 3.2开始,CGLIB被从新打包并蕴含在
spring-core
JAR中。换句话说,基于CGLIB的AOP就像JDK动静代理一样“开箱即用”。
CGLIB代理和动静代理之间简直没有性能差别。
在这种状况下,性能不应作为决定性的思考因素。
6.4.6 应用全局
Advisors
通过向拦截器名称附加星号,所有具备与星号之前的局部相匹配的bean名称的advisor
都会被增加到advisor
链中。如果你须要增加一组规范的全局advisor
,这将十分有用。以下示例定义了两个全局advisor
程序:
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target" ref="service"/> <property name="interceptorNames"> <list> <value>global*</value> </list> </property></bean><bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/><bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>
参考代码:com.liyong.ioccontainer.starter.ProxyFactoryBeanIocContainer
6.5 简洁的代理定义
特地是在定义事务代理时,你可能会失去许多相似的代理定义。应用父bean和子bean定义以及外部bean定义能够产生更洁净、更简洁的代理定义。
首先,咱们为代理创立父模板,bean定义,如下所示:
<bean id="txProxyTemplate" abstract="true" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager" ref="transactionManager"/> <property name="transactionAttributes"> <props> <prop key="*">PROPAGATION_REQUIRED</prop> </props> </property></bean>
它自身从未实例化,因而实际上可能是不残缺的。而后,须要创立的每个代理都是一个子bean定义,它将代理的指标包装为一个外部bean定义,因为无论如何指标都不会独自应用。以下示例显示了这样的子bean:
<bean id="myService" parent="txProxyTemplate"> <property name="target"> <bean class="org.springframework.samples.MyServiceImpl"> </bean> </property></bean>
你能够从父模板笼罩属性。在以下示例中,咱们将笼罩事务流传设置:
<bean id="mySpecialService" parent="txProxyTemplate"> <property name="target"> <bean class="org.springframework.samples.MySpecialServiceImpl"> </bean> </property> <property name="transactionAttributes"> <props> <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="store*">PROPAGATION_REQUIRED</prop> </props> </property></bean>
请留神,在父bean的示例中,咱们通过将abstract
属性设置为true
来将父bean定义显式标记为形象,如前所述,因而实际上可能不会实例化它。默认状况下,应用程序上下文(但不是简略的bean工厂)预实例化所有单例。因而,重要的是(至多对于单例bean),如果你有一个(父)bean定义仅打算用作模板,并且此定义指定了一个类,则必须确保将abstract
属性设置为true
。否则,应用程序上下文实际上会尝试对其进行实例化。
6.6 通过ProxyFactory
编程式地创立AOP代理
应用Spring以编程形式创立AOP代理很容易。这使你能够应用Spring AOP,而无需依赖Spring IoC。
由指标对象实现的接口将被主动代理。以下清单显示了应用一个拦截器和一个advisor
为指标对象创立代理的过程:
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);factory.addAdvice(myMethodInterceptor);factory.addAdvisor(myAdvisor);MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();
第一步是结构一个类型为org.springframework.aop.framework.ProxyFactory
的对象。你能够应用指标对象(如后面的示例所示)来创立它,也能够在代替构造函数中指定要代理的接口。
你能够增加告诉(拦截器是一种专门的告诉)、advisors
,或者同时增加它们,并在ProxyFactory
的生命周期中操作它们。如果增加了IntroductionInterceptionAroundAdvisor
,则能够使代理实现其余接口。
ProxyFactory
上还有一些不便的办法(继承自AdvisedSupport
),能够增加其余告诉类型,比方before
和throw advice
。AdvisedSupport
是ProxyFactory
和ProxyFactoryBean
的超类。
在大多数应用程序中,将AOP代理创立与IoC框架集成在一起是最佳实际。通常,倡议你应用AOP从Java代码内部化配置。
6.7 操作告诉对象
无论如何创立AOP代理,都能够通过应用org.springframework.aop.framework.Advised
接口来操作它们。任何AOP代理都能够转换到这个接口,不论它实现了哪个接口。该接口蕴含以下办法:
Advisor[] getAdvisors();void addAdvice(Advice advice) throws AopConfigException;void addAdvice(int pos, Advice advice) throws AopConfigException;void addAdvisor(Advisor advisor) throws AopConfigException;void addAdvisor(int pos, Advisor advisor) throws AopConfigException;int indexOf(Advisor advisor);boolean removeAdvisor(Advisor advisor) throws AopConfigException;void removeAdvisor(int index) throws AopConfigException;boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;boolean isFrozen();
getAdvisors()
办法为已增加到工厂的每个advisor
、拦截器或其余告诉类型返回一个advisor
。如果增加了advisor
,则此索引处返回的advisor
是你增加的对象。如果增加了拦截器或其余告诉类型,Spring会将其包装在带有指向总是返回true
的切入点的advisor
中。因而,如果你增加一个MethodInterceptor
,为这个索引返回的advisor
是一个DefaultPointcutAdvisor
,它返回你的MethodInterceptor
和一个匹配所有类和办法的切入点。
addAdvisor()
办法可用于增加任何Advisor
。通常,持有切入点和告诉的advisor
是通用的DefaultPointcutAdvisor
,你能够将其用于任何告诉或切入点(但不用于introduction
)。
默认状况下,即便已创立代理,也能够增加或删除advisor
或拦截器。惟一的限度是不可能增加或删除一个introduction
advisor
,因为工厂中的现有代理不会显示接口更改。(你能够从工厂获取新的代理来防止此问题。)
以下示例显示了将AOP代理投射到Advised
接口并检查和解决其告诉:
Advised advised = (Advised) myObject;Advisor[] advisors = advised.getAdvisors();int oldAdvisorCount = advisors.length;System.out.println(oldAdvisorCount + " advisors");// Add an advice like an interceptor without a pointcut// Will match all proxied methods// Can use for interceptors, before, after returning or throws adviceadvised.addAdvice(new DebugInterceptor());// Add selective advice using a pointcutadvised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);
在生产中批改业务对象上的告诉是否可取(没有双关语)值得狐疑,只管毫无疑问存在非法的应用案例。然而,它在开发中(例如在测试中)十分有用。有时咱们发现以拦截器或其余告诉的模式增加测试代码,并进入咱们要测试的办法调用中十分有用。(例如,在标记回滚事务之前,告诉能够进入为该办法创立的事务,可能是为了运行SQL来查看数据库是否被正确更新。)
依据创立代理的形式,通常能够设置解冻标记。在这种状况下,Advised
isFrozen()
办法返回true
,并且任何通过增加或删除来批改告诉的尝试都会导致AopConfigException
。解冻已告诉对象状态的能力在某些状况下十分有用(例如,避免调用代码删除平安拦截器)。
6.8 应用“主动代理
”性能
到目前为止,咱们曾经思考过通过应用ProxyFactoryBean
或相似的工厂bean来显式创立AOP代理。
Spring还容许咱们应用“主动代理
” Bean定义,该定义能够主动代理选定的Bean定义。它构建在Spring的bean后处理器基础设施上,该基础设施容许在装载容器时批改任何bean定义。
在这个模型中,你在XML bean定义文件中设置了一些非凡的bean定义来配置主动代理基础设施。这使你能够申明有资格进行主动代理的指标。你无需应用ProxyFactoryBean
。
有两种办法能够做到这一点:
- 通过应用在以后上下文中援用特定bean的主动代理创立器。
- 主动代理创立的一个非凡状况值得独自思考:由源码级别元数据属性驱动的主动代理创立。
6.8.1 自定代理Bean定义
本节介绍了org.springframework.aop.framework.autoproxy
包提供的主动代理创立器。
BeanNameAutoProxyCreator
BeanNameAutoProxyCreator
类是一个BeanPostProcessor
,能够主动为名称与文字值或通配符匹配的bean创立AOP代理。以下示例显示了如何创立BeanNameAutoProxyCreator
bean:
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="beanNames" value="jdk*,onlyJdk"/> <property name="interceptorNames"> <list> <value>myInterceptor</value> </list> </property></bean>
与ProxyFactoryBean
一样,有一个interceptorNames
属性而不是一列拦截器,以容许原型advisors
的正确行为。名为“拦截器”的能够是advisors
或任何告诉类型。
一般而言,与主动代理一样,应用BeanNameAutoProxyCreator
的要点是将雷同的配置统一地利用于多个对象,并且配置量起码。将申明式事务利用于多个对象是一种风行的抉择。
名称匹配的Bean定义,例如后面示例中的jdkMyBean
和onlyJdk
,是带有指标类的一般旧Bean定义。BeanNameAutoProxyCreator
主动创立一个AOP代理。雷同的告诉实用于所有匹配的bean。留神,如果应用了advisors
(而不是后面的示例中的拦截器),则切入点可能会不同地利用于不同的bean。
DefaultAdvisorAutoProxyCreator
DefaultAdvisorAutoProxyCreator
是更通用,性能极其弱小的主动代理创立器。这将主动在以后上下文中利用合格的advisor
,而不须要在主动代理advisor
bean定义中蕴含特定的bean名称。与BeanNameAutoProxyCreator
一样,它具备统一的配置和防止反复的长处。
应用此机制波及:
- 指定
DefaultAdvisorAutoProxyCreator
bean定义。 - 在雷同或关联的上下文中指定任何数量的
advisor
。请留神,这些必须是advisor
,而不是拦截器或其余告诉。这是必要的,因为必须有一个要评估的切入点来查看每个告诉到候选bean定义的资格。DefaultAdvisorAutoProxyCreator
主动评估每个advisor
中蕴含的切入点,以查看应该将什么(如果有的话)告诉利用到每个业务对象(例如示例中的businessObject1
和businessObject2
)。
这意味着能够将任意数量的advisor
主动利用于每个业务对象。如果任何advisor
中没有切入点匹配业务对象中的任何办法,则该对象不会被代理。当为新的业务对象增加Bean定义时,如有必要,它们会主动被代理。
通常,主动代理的长处是使调用者或依赖者无奈取得未告诉的对象。在此ApplicationContext
上调用getBean(“ businessObject1”)
会返回AOP代理,而不是指标业务对象。(后面显示的“ inner
bean”也提供了这一益处。)
以下示例创立一个DefaultAdvisorAutoProxyCreator
bean和本节中探讨的其余元素:
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/><bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor"> <property name="transactionInterceptor" ref="transactionInterceptor"/></bean><bean id="customAdvisor" class="com.mycompany.MyAdvisor"/><bean id="businessObject1" class="com.mycompany.BusinessObject1"> <!-- Properties omitted --></bean><bean id="businessObject2" class="com.mycompany.BusinessObject2"/>
如果要将雷同的告诉统一地利用于许多业务对象,则DefaultAdvisorAutoProxyCreator
十分有用。一旦基础设施定义就位,你就能够增加新的业务对象,而不包含特定的代理配置。你还能够很容易地删除其余切面(例如,跟踪或性能监督切面),只需对配置进行最小的更改。DefaultAdvisorAutoProxyCreator
反对过滤(通过应用命名约定,只有特定的advisor
被评估,这容许在同一个工厂中应用多个不同配置的AdvisorAutoProxyCreators
)和排序。Advisor
能够实现org.springframework.core.Ordered
接口,以确保在呈现问题时能够正确排序。后面示例中应用的TransactionAttributeSourceAdvisor
具备可配置的程序值。默认设置为无序。
参考代码:com.liyong.ioccontainer.starter.AdvisorAutoProxyCreatorIocContainer
6.9 应用TargetSource
实现
Spring提供了TargetSource
的概念,以org.springframework.aop.TargetSource
接口示意。该接口负责返回实现连接点的“指标对象
”。每当AOP代理解决办法调用时,就会向TargetSource
实现询问指标实例。
应用Spring AOP的开发人员通常不须要间接应用TargetSource
实现,然而这提供了反对池、热交换和其余简单指标的弱小办法。例如,通过应用池来治理实例,TargetSource
能够为每次调用返回不同的指标实例。
如果未指定TargetSource
,则将应用默认实现包装本地对象。每次调用都返回雷同的指标(与你冀望的一样)。
本节的其余部分形容了Spring随附的规范指标源以及如何应用它们。
应用自定义指标源时,指标通常须要是原型而不是单例bean定义。这样,Spring能够在须要时创立一个新的指标实例。
6.9.1 可热交换指标源
org.springframework.aop.target.HotSwappableTargetSource
的存在让AOP代理指标被切换,同时让调用者放弃对它的援用。
扭转指标源的指标立刻失效。HotSwappableTargetSource
是线程平安的。
你能够在HotSwappableTargetSource
上通过应用swap()
办法扭转指标,相似上面例子展现:
HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");Object oldTarget = swapper.swap(newTarget);
上面的例子显示所须要的XML定义:
<bean id="initialTarget" class="mycompany.OldTarget"/><bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource"> <constructor-arg ref="initialTarget"/></bean><bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="targetSource" ref="swapper"/></bean>
后面的swap()
调用更改了可替换bean的指标。持有对该bean的援用的客户端不晓得更改,但会立刻开始达到新指标。
只管这个示例没有增加任何告诉(应用TargetSource
不须要增加告诉),然而能够将任何TargetSource
与任意告诉联合应用。
6.9.2 池指标源
应用池指标源能够提供相似于无状态会话ejb的编程模型,其中保护雷同实例的池,办法调用将开释池中的对象。
Spring池和SLSB
池之间的要害区别在于,Spring池能够利用于任何POJO。与Spring个别状况一样,能够以非侵入性的形式利用此服务。
Spring提供对 Commons Pool 2.2
反对,它提供一个相当地高效池实现。你须要在你的利用类门路上增加commons-pool
jar去应用这个个性。你也能够应用org.springframework.aop.target.AbstractPoolingTargetSource
去反对其余的池化API。
还反对Commons Pool 1.5+,但从Spring Framework 4.2开始不举荐应用。
以下清单显示了一个示例配置:
<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject" scope="prototype"> ... properties omitted</bean><bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPool2TargetSource"> <property name="targetBeanName" value="businessObjectTarget"/> <property name="maxSize" value="25"/></bean><bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="targetSource" ref="poolTargetSource"/> <property name="interceptorNames" value="myInterceptor"/></bean>
请留神,指标对象(在后面的示例中为businessObjectTarget
)必须是原型。这使PoolingTargetSource
实现能够创立指标的新实例,以依据须要去扩大池中对象。无关其属性的信息,请参见AbstractPoolingTargetSource的javadoc和心愿应用的具体子类maxSize
是最根本的,并且始终保障存在。
在这种状况下,myInterceptor
是须要在同一IoC上下文中定义的拦截器的名称。然而,你无需指定拦截器即可应用池。如果只心愿池化而没有其余告诉,则齐全不要设置interceptorNames
属性。
你能够将Spring配置为可能将任何池化对象转换到org.springframework.aop.target.PoolingConfig
接口,该接口通过introduction
来公开无关池的配置和以后大小的信息。
你须要定义相似于以下内容的advisor
:
<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="targetObject" ref="poolTargetSource"/> <property name="targetMethod" value="getPoolingConfigMixin"/></bean>
通过在AbstractPoolingTargetSource
类上调用便捷办法来取得此advisor
,因而能够应用MethodInvokingFactoryBean
。该advisor
的名称(在此处为poolConfigAdvisor
)必须位于裸露池对象的ProxyFactoryBean
中的拦截器名称列表中。
转换的定义如下:
PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");System.out.println("Max pool size is " + conf.getMaxSize());
池化无状态的服务对象通常是不必要的。咱们不认为它应该是默认抉择,因为大多数无状态对象天然是线程平安的,并且如果缓存了资源,实例池会成问题。
通过应用主动代理,能够实现更简略的池化。你能够设置任何主动代理创建者应用的TargetSource
实现。
6.9.3 原型指标源
设置“原型
”指标源相似于设置池化TargetSource
。在这种状况下,每次办法调用都会创立指标的新实例。只管在古代JVM中创立新对象的老本并不高,但连贯新对象(满足其IoC依赖项)的老本可能会更高。因而,没有充沛的理由就不应应用此办法。
为此,你能够批改后面显示的poolTargetSource
定义,如下所示(为分明起见,咱们也更改了名称):
<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource"> <property name="targetBeanName" ref="businessObjectTarget"/></bean>
惟一的属性是指标Bean的名称。在TargetSource
实现中应用继承来确保命名统一。与池化指标源一样,指标bean必须是原型bean定义。
6.9.4 ThreadLocal
指标源
如果须要为每个传入申请(每个线程)创立一个对象,则ThreadLocal
指标源很有用。ThreadLocal
的概念提供了JDK范畴的性能,能够通明地将资源与线程一起存储。设置ThreadLocalTargetSource
简直与针对其余类型的指标源所阐明的雷同,如以下示例所示:
<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource"> <property name="targetBeanName" value="businessObjectTarget"/></bean>
在多线程和多类加载器环境中谬误地应用ThreadLocal
实例时,会带来重大的问题(可能导致内存透露)。你应该始终思考在其余一些类中包装threadlocal
,并且相对不要间接应用ThreadLocal
自身(包装类中除外)。另外,你应该始终记住正确设置和勾销设置线程本地资源的正确设置和勾销设置(后者仅波及对ThreadLocal.set(null)
的调用)。在任何状况下都应进行勾销设置,因为不勾销设置可能会导致呈现问题。Spring的ThreadLocal
反对为你做到了这一点,应该始终思考应用ThreadLocal
实例,无需其余适当的解决代码。
6.10 定义新告诉类型
Spring AOP被设计为可扩大的。尽管目前在外部应用拦挡实现策略,然而除了在盘绕告诉、前置告诉、异样告诉以及在返回告诉进行拦挡外,还能够反对任意的告诉类型。
适配器包是一个SPI包,它容许在不更改外围框架的状况下增加对新的自定义告诉类型的反对。对自定义Advice
类型的惟一限度是它必须实现org.aopalliance.aop.Advice
标记接口。
无关更多信息,请参见org.springframework.aop.framework.adapter javadoc。
参考代码:com.liyong.ioccontainer.starter.TargetSourceIocContainer
作者
集体从事金融行业,就任过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就任于某银行负责对立领取零碎建设。本身对金融行业有强烈的喜好。同时也实际大数据、数据存储、自动化集成和部署、散布式微服务、响应式编程、人工智能等畛域。同时也热衷于技术分享创建公众号和博客站点对常识体系进行分享。关注公众号:青年IT男 获取最新技术文章推送!
博客地址: http://youngitman.tech
CSDN: https://blog.csdn.net/liyong1...
微信公众号:
技术交换群: