上一章通过@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代理通过在运行时生成目标类的子类来工作。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 advice
advised.addAdvice(new DebugInterceptor());
// Add selective advice using a pointcut
advised.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/liyong1028826685
微信公众号:
技术交流群: