1.AOP
IOC容器是Spring的基础,而AOP面向切面编程(Aspect-Oriented Programming)则是Spring作为一个开发平台实现模块的体现,其将业务代码和一些公用的功能相分离,使得用户在开发过程中不用考虑这些公用功能,如:一些日志记录、事务控制等。
本篇我们将如下组织:先介绍AOP的一些基础知识,然后介绍AOP实现的几种方式,其次介绍Jdk代理模式,最后再介绍AOP的实现原理
2.AOP基础知识
通知(Advice):定义在连接点做什么,为切面提供织入的方法。通俗的理解即一些共有方法的实现类,如日志记录功能、事务功能。常见的通知类型有BeforeAdvice、AfterAdvice、ThrowAdvice等。
连接点(Pointcut):定义通知被使用的一个“时机”,这些“时机”就是连接点,例如方法被调用时、异常被抛出时等等。
切点(Pointcut):决定Advice应用于哪个地方应用切面,常是某个接口的方法或者某个类的方法等。连接点指代通知应用的时机,切入点指代通知应用应用的地方,通知是指代事件。连接点、切入点、通知描述了一个事件发生的时间、地点、事件。具体例子来说:连接点是指代通知是该应用于方法调用前还是调用后的问题,切点是指代通知会应用到哪些方法上。
通知器(Advisor):用于结合通知和切点来描述一个AOP的功能,其作用是描述哪个通知应该应用于哪个切点上面,其作用等同于切面。
切面(Aspect):通知和切点组成一个切面,是描述事件发生的时间、地点和事件。
引入(Introduction):用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。
目标对象(Target Object): 被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。
AOP代理(AOP Proxy):用于将切面和被通知对象功能结合的对象,用于替代被通知对象,每一次对于被通知的对象的调用实际上是调用的代理对象,代理对象通过调用切面之后,再调用被通知的对象,从而达到AOP目的。AOP代理可以是JDK动态代理或者CGLIB代理。
织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
3.AOP实现的几种方式
3.1 纯POJO对象的Aspect切面
//POJO Advise对象:这里包含了Advise通知和JoinPoint连接点两个概念
//JoinPoint包含了切面应用到的被通知对象方法和参数等
@Service
public class MethodAdvice {
private static final Logger LOGGER= LoggerFactory.getLogger(MethodAdvice.class);
public void beforeMethod(JoinPoint joinPoint){
LOGGER.info(joinPoint.toLongString());
LOGGER.info("方法开始调用");
}
public void afterMethod(){
LOGGER.info("方法调用结束");
}
}
//这里包含了切点PointCut、Adspect切面、Advise通知、target目标等对象
//这是一个完整的AOP功能,其中<aop:config>下面的对象都是全局的AOP对象,即该PointCut在另外一个<aop:config>下面也可以使用
//PointCut中的com.job.search.*.service.impl.*.*(..)表达的意义是
//*表示任意的英文字符,该处意思就是:匹配
//com.job.search.*.service.impl包下的*.*(..)任何类中的任何方法,并且任意参数
<aop:config>
<aop:pointcut id="myPointcut"
expression="((execution(* com.job.search.*.service.impl.*.*(..)))
|| (execution(* com.job.search.*.*.service.*.*(..)))
)"/>
<aop:aspect ref="methodAdvice">
<aop:before method="beforeMethod" pointcut-ref="myPointcut"></aop:before>
<aop:after method="afterMethod" pointcut-ref="myPointcut"></aop:after>
</aop:aspect>
</aop:config>
对于AOP除了使用切面来定义AOP行为外,还可以通过结合通知类型接口BeforeAdvice、AfterAdvice、ThrowAdvice来达到的目的
//MethodBeforeAdvice接口中的before方法会在target方法前执行。
@Service
public class DefaultMethodAdvice implements MethodBeforeAdvice {
private static final Logger LOGGER= LoggerFactory.getLogger(DefaultMethodAdvice.class);
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
LOGGER.info("aop advisor before running");
}
}
//xml配置文件
<aop:config>
<aop:pointcut id="myPointcut"
expression="((execution(* com.job.search.*.service.impl.*.*(..)))
|| (execution(* com.job.search.*.*.service.*.*(..)))
)"/>
<aop:advisor advice-ref="defaultMethodAdvice" pointcut-ref="myPointcut"></aop:advisor>
</aop:config>
3.2 全注解的AOP实现
除了上述方法外,AOP也有纯注解的方式,达到零配置文件
//定义切面Bean
@Service
@Aspect //定义切面类
public class AnnotationMethodAdvice {
private static final Logger LOGGER= LoggerFactory.getLogger(AnnotationMethodAdvice.class);
////定义切入点,该方法名字就是PointCut的id
@Pointcut("(execution(* com.job.search.*.service.impl.*.*(..))) || (execution(* com.job.search.*.*.service.*.*(..)))")
private void myPointcut(){}
//指定PointCut的前置通知
@Before("myPointcut()")
public void beforeMethod(JoinPoint joinPoint){
LOGGER.info(joinPoint.toLongString());
LOGGER.info("方法开始调用");
}
//指定PointCut的后置通知
@After("myPointcut()")
public void afterMethod(){
LOGGER.info("方法调用结束");
}
}
//配置文件只需要启动自动注释的扫描功能即可
<!--启用Aspect相关标签功能 -->
<aop:aspectj-autoproxy/>
<!-- 启动Spring MVC功能 -->
<mvc:annotation-driven />
<!-- 自动扫描该包,使SpringMVC认为包下用了@controller注解的类是控制器 -->
<context:component-scan base-package="com.job.search.**"/>
上面的代码是不是非常简便,并且没有在xml中配置任何一个bean。我个人而言倾向于该种方法,彻底的将切面和业务逻辑分离开来,并且要扩展功能也十分的方便
3.3 经典AOP代理实现
所谓经典就是Spring本身集成的AOP功能,但经典本身而言也是过时的意思,反正本人不推荐用这个方式比较的繁琐,但是在大部分Spring的书籍中都是以介绍该种方式来介绍AOP功能,anyway , 接下来让我们看看最原始的AOP实现方式。
//Advise通知类
public class ClassicMethodAdvice implements MethodBeforeAdvice,AfterReturningAdvice {
private static final Logger LOGGER= LoggerFactory.getLogger(ClassicMethodAdvice.class);
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
LOGGER.info("This is classic beforeAdvice");
}
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
LOGGER.info("This is classic afterAdvice");
}
}
//xml配置
<bean id="classicPointCut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="pattern" value=".*find.*"/>
</bean>
<bean id="classicAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice" ref="classicMethodAdvice"/>
<property name="pattern" value=".*find.*"/>
</bean>
这样的配置有几点不好的地方:1.使用正则表示进行匹配容易匹配到不需要的类中的方法,如这里会匹配所有含有find字符串的方法,这个一定不会是你想要的,一般只会是service类中的find方法。
2.就是配置Bean较多,因为pointcut、advisor是不能通过注解的方式解决的
4.Java动态代理
Java动态代理最常用的功能就是在被代理对象的方法调用过程中加入自己的代码,并且消除为了对实现不同接口的对象,建立不同的代理类的烦恼。
//代理1
public interface InterfaceA {
void doSameThing();
}
public class ObjectA implements InterfaceA {
@Override
public void doSameThing() {
}
}
public class ProxyA {
private InterfaceA interfaceA;
public void doSameThing(){
//开始事务
interfaceA.doSameThing();
//提交事务
}
}
//代理2
public interface InterfaceB {
void doSameThing();
}
public class ObjectB implements InterfaceB {
@Override
public void doSameThing() {
}
}
public class ProxyB {
private InterfaceB interfaceB;
public void doSameThing(){
//开始事务
interfaceB.doSameThing();
//提交事务
}
}
如上面的代码所示,其上面的ProxyA和ProxyB都是代理类,其附加的功能都是保证两个接口的方法都是有事务的,如果有很多这样的类,那么就会产生很多这样的代理类,使得代码难以管理,并且保证事务的代码和业务逻辑耦合在一起。对于多个代理类和业务逻辑与事务代码耦合的问题我们可以通过Java的动态代理来解决。
public class MyInvocationHandler implements InvocationHandler {
// 目标对象
private Object target;
// 通知
private ClassicMethodAdvice classicMethodAdvice;
public MyInvocationHandler(Object target, ClassicMethodAdvice methodAdvice) {
this.target = target;
this.classicMethodAdvice = methodAdvice;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//开始前置通知
classicMethodAdvice.before(method, args, target);
//业务逻辑代码
Object result = method.invoke(target, args);
//后置通知
classicMethodAdvice.afterReturning(result, method, args, target);
return result;
}
public Object getProxy() {
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
target.getClass().getInterfaces(), this);
}
public static void main(String[] args) {
ClassicMethodAdvice classicMethodAdvice=new ClassicMethodAdvice();
MyInvocationHandler invocationHandlerA=new MyInvocationHandler(new ObjectA(),classicMethodAdvice);
InterfaceA objectA= (InterfaceA) invocationHandlerA.getProxy();
MyInvocationHandler invocationHandlerB=new MyInvocationHandler(new ObjectB(),classicMethodAdvice);
InterfaceB objectB= (InterfaceB) invocationHandlerB.getProxy();
objectA.doSameThing();
objectB.doSameThing();
}
}
从上面的代码可以看出现在的ObjectA和ObjectB已经不是new产生的匿名对象,而是通过代理产生的对象并且对其方法的调用都已经加入了事务的逻辑在里面。
注:Java动态代理有两种:Jdk动态代理和Cglib动态代理两种。Jdk动态代理是基于接口的,即代理对象必须实现接口才能使用。而Cglib动态代理可以直接作用于类对象,对于具体的实现不在此叙述,如果有兴趣可以在网上搜索有很多这样的文章。
5.AOP实现原理
如果你看懂了上面关于动态代理章节的介绍,其实你已经发现了AOP的基本构架了,只不过AOP是通过从注解或者XML文件中解析出产生动态代理对象时需要的接口、目标对象、方法、参数以及Advise对象。
在JdkDynamicAopProxy对象的invoke方法中有如下一段代码,retVal = invocation.proceed();该代码会沿着我们的Advisor对象链调用所有的advise对象方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodInvocation invocation;
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource;
Class targetClass = null;
Object target = null;
try {
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement the equals(Object) method itself.
return equals(args[0]);
}
if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
// The target does not implement the hashCode() method itself.
return hashCode();
}
if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}
Object retVal;
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// May be null. Get as late as possible to minimize the time we "own" the target,
// in case it comes from a pool.
target = targetSource.getTarget();
if (target != null) {
targetClass = target.getClass();
}
// Get the interception chain for this method.
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
// Check whether we have any advice. If we don't, we can fallback on direct
// reflective invocation of the target, and avoid creating a MethodInvocation.
if (chain.isEmpty()) {
// We can skip creating a MethodInvocation: just invoke the target directly
// Note that the final invoker must be an InvokerInterceptor so we know it does
// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args);
}
else {
// We need to create a method invocation...
invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();
}
// Massage return value if necessary.
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
// Special case: it returned "this" and the return type of the method
// is type-compatible. Note that we can't help if the target sets
// a reference to itself in another returned object.
retVal = proxy;
} else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException("Null return value from advice does not match primitive return type for: " + method);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
//拦截器对象(advise)的调用
public Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
}
else {
// Dynamic matching failed.
// Skip this interceptor and invoke the next in the chain.
return proceed();
}
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
//在调用完所有的拦截链对象后,调用target对象的方法
protected Object invokeJoinpoint() throws Throwable {
return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments);
}
public static Object invokeJoinpointUsingReflection(Object target, Method method, Object[] args)
throws Throwable {
// Use reflection to invoke the method.
try {
ReflectionUtils.makeAccessible(method);
return method.invoke(target, args);
}
catch (InvocationTargetException ex) {
// Invoked method threw a checked exception.
// We must rethrow it. The client won't see the interceptor.
throw ex.getTargetException();
}
catch (IllegalArgumentException ex) {
throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" +
method + "] on target [" + target + "]", ex);
}
catch (IllegalAccessException ex) {
throw new AopInvocationException("Could not access method [" + method + "]", ex);
}
}
从上面看出其本质上对于AOP后的对象调用实际上是调用的AopProxy对象,其会在调用完所有的advise对象后调用目标对象的方法。
最后,我们将单独拿出一篇文章来写AOP来实现Service方法的事务性并且介绍一些事务的知识,如什么是只读事务,其原理等