本文旨在将清楚AOP是什么,以及Spring AOP具体如何使用,有何注意事项。
我们先来看一个iphone:
在APPLE早期,大概1977年的时候,APPLE刚刚起步,规模不是很大,对于APPLE来说,它有一个总工厂,它主要负责一下这些事务:
对这个APPLE这个总工厂来说,它需要在保证APPLE的核心产品和服务标准的情况下,进行生产产品,研发产品,销售,售后四大主要的业务。
随着APPLE的不断壮大,它的各个业务都变得异常庞大,单单靠APPLE总工厂来处理这些业务的话,已经吃不消了,所以,它需要对现在的这个业务体制进行改革。
改革的方式是:请来代理商,让它们来对产品进行销售和售后,自己只负责生产产品和研发,但是它们也必须保证各个方面符合APPLE的标准。
随着时间的推移,代理商也不满足于销售和售后了,于是,它可以在原本APPLE总工厂没有这项服务的前提下,提供一些个性化服务,比如,碎屏险,贴膜,手机壳。
现在,你需要购买APPLE的任何产品,只需要在代理商那去买,可以同样享受APPLE的服务,而且还会有个性化的服务。
以上的这个例子就是代理模式的思想。
我们来看一下什么是代理模式:
代理模式(英语:Proxy Pattern)是程序设计中的一种设计模式。
代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
通俗一点来理解代理模式:本来由A做的事情,现在由B进行代理,并且可以对A的方法进行增强。以前访问一个对象需要通过A,现在只需要通过B就可以进行对该对象的访问。
在Java中,使用代理的方式有两种,静态代理和动态代理:
下面是两种方式的代码演示,仅演示了需要代理的部分。
首先确定一下接口:
public interface IAppleCore {
//销售
void sale();
//售后
void saleAfter();
}
然后是工厂类,需要实现接口:
public class AppleFactory implements IAppleCore {
@Override
public void sale() {
System.out.println("销售了产品");
}
@Override
public void saleAfter(){
System.out.println("进行了售后服务");
}
}
然后是代理类,同样需要实现接口:
public class AppleProxy implements IAppleCore {
private IAppleCore target=new AppleFactory();
@Override
public void sale() {
System.out.println("代理已开启");
target.sale();
System.out.println("代理结束");
}
@Override
public void saleAfter() {
System.out.println("代理已开启");
target.saleAfter();
System.out.println("代理结束");
}
}
最后main方法测试一下:
public class Client {
public static void main(String[] args) {
//使用代理对象
IAppleCore proxy=new AppleProxy();
//使用代理对象执行方法
proxy.sale();
proxy.saleAfter();
}
}
动态代理的核心是使用反射创建对象。
动态代理用到了JDK官方提供的Proxy
类。
使用jdk生成的动态代理的前提是目标类必须有实现的接口。
在这个代理的过程中JDK动态生成了一个类去实现接口。
newProxyInstance
方法参数说明:
ClassLoader
:类加载器,用于加载被代理对象的字节码,与被代理对象使用相同的类加载器。Claa[]
:字节码数组,让代理对象和被代理对象有相同的方法。InvocationHandler
:提供增强的代码,通常采用匿名内部类,谁使用,谁写此类。接口和工厂类与上面一致,不同的是代理类:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class AppleProxy{
private Object target;
public AppleProxy(IAppleCore target) {
this.target = target;
}
public Object getProxyInstance(){
Object proxy= Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
//增强的方法
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName=method.getName();
Object result=null;
if("saleAfter".equals(methodName)){
System.out.println("检测到售后方法,开始代理");
result=method.invoke(target,args);
System.out.println("代理成功");
}else{
result=method.invoke(target,args);
}
return result;
}
}
);
return proxy;
}
}
最后测试一下:
public class Client {
public static void main(String[] args) {
IAppleCore target=new AppleFactory();
//代理对象
IAppleCore proxy=(IAppleCore)new AppleProxy(target).getProxyInstance();
System.out.println("target:"+target.getClass());
System.out.println("proxy:"+proxy.getClass());
proxy.sale();
proxy.saleAfter();
}
}
使用JDK动态代理需要目标类必须有实现的接口。如果需要去代理一个普通的类,需要使用Cglib提供的Enhancer
类。
使用Cglib的话,被代理类不能被final
修饰,既需要可以创建子类
Cglib是以动态生成的子类继承目标的方式实现,在运行期动态的在内存中构建一个子类。
create
方法参数:
Class
:指定被代理对象的字节码。Callback
:提供增强的方法。import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class Client {
public static void main(String[] args) {
final IAppleCore target=new AppleFactory();
IAppleCore proxy=(IAppleCore) Enhancer.create(target.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object oproxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
String methodName=method.getName();
Object result=null;
if("saleAfter".equals(methodName)){
System.out.println("检测到售后方法,开始代理");
result=method.invoke(target,args);
System.out.println("代理成功");
}else{
result=method.invoke(target,args);
}
return result;
}
});
System.out.println("target:"+target.getClass());
System.out.println("proxy:"+proxy.getClass());
proxy.sale();
proxy.saleAfter();
}
}
在明白了上述代理,动态代理的远离和优势之后,对于AOP就好理解了,因为AOP的核心思想就是动态代理。
我们先来看一下官方的一些术语:
看了这些术语后,可能关于代理的部分好理解了,但是,切面与这些切入点,连接点怎么处理呢?
我们来看一个例子,假如你现在需要对数据库里面的信息做一个操作:将所有男生的财产增加100万,那么对于的关系应该是下面这样的。
总结一下对AOP的理解,AOP其实就是面对这个切面来编写程序,选择需要增强的对象(切入点),然后增强(通知)。
在配置Spring AOP前,先把需要使用到的类配置到相关的IOC容器中。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="accountService" class="cservice.impl.ATServiceImpl">bean>
<bean id="logger" class="utils.Loggers">bean>
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<aop:before method="printLog" pointcut="execution(* service.impl.*.*(..))">aop:before>
aop:aspect>
aop:config>
beans>
表示开始进行Spring AOP的配置。
aop:aspect
表示配置切面
id
:切面提供一个唯一标识。ref
:指定通知类bean的id。aop:aspect
标签的内部需要使用对应标签来配置通知的类型。aop:before
:用于配置前置通知。指定增强的方法在切入点方法之前执行。
method
:用于指定通知类中的增强方法名称。ponitcut-ref
:用于指定切入点的表达式的引用。poinitcut
:用于指定切入点表达式。aop:after-returning
:用于配置后置通知。切入点方法正常执行之后。它和异常通知只能有一个执行。属性同上。
aop:after-throwing
:用于配置异常通知。切入点方法执行产生异常后执行。它和后置通知只能执行一个 。属性同上。
aop:after
:于配置最终通知。无论切入点方法执行时是否有异常,它都会在其后面执行。属性同上。
aop:around:
用于配置环绕通知。它是 spring框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。属性同上。
ProceedingJoinPoint
,它可以作为环绕通知的方法参数。在环绕通知执行时,spring框架会为我们提供该接口的实现类对象,我们可以直接使用。<aop:around method="aroundPringLog" pointcut-ref="pt1">aop:around>
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("后置");
return rtValue;
}catch (Throwable t){
System.out.println("异常");
throw new RuntimeException(t);
}finally {
System.out.println("最终");
}
}
在书写切入点表达式前需要加上关键字execution(表达式)
。
可以单独配置标签(写在当前切面标签内也可以写在切面外,不过要写在切面之前),也可以直接写在通知标签内,单独配置标签方式如下:
标准切入点表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
例如:public void com.atfwus.service.impl.ATServiceImpl.proxy()
省略访问修饰符:
例如:void com.atfwus.service.impl.ATServiceImpl.proxy()
返回值可以使用通配符,表示任意返回值:
例如:* com.atfwus.service.impl.ATServiceImpl.proxy()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*
:
例如:* *.*.*.*.ATServiceImpl.proxy()
包名可以使用..
表示当前包及其子包:
例如:* *..ATServiceImpl.proxy()
类名和方法名都可以使用*
来实现通配:
例如:* *..*.*()
参数的写法:
int
java.lang.String
..
表示有无参数均可,有参数可以是任意类型全通配切入点表达式:* *..*.*(..)
一般切到业务层实现类下的所有方法,如* com.atfwus.service.impl.*.*(..)
先在xml进行相关的配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.atfwus">context:component-scan>
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
beans>
@Aspect
注解声明为切面 :@Aspect
注解表示把当前类声明为切面类。@Before
:把当前方法看成是前置通知。属性value
:用于指定切入点表达式,还可以指定切入点表达式的引用。@AfterReturning
:把当前方法看成是后置通知。 属性同上。@AfterThrowing
:把当前方法看成是异常通知。属性同上。@After
:把当前方法看成是最终通知。属性同上。@Around
:把当前方法看成是环绕通知。属性同上。@Pointcut
:value
:指定表达式的内容。@Pointcut("execution(* com.atfwus.service.impl.*.*(..))")
private void pt1() {}
@EnableAspectJAutoProxy
注解就可以了。在上面我们使用动态代理的时候,使用到了两种模式的动态代理,一种是基于JDK的,需要实现接口,一种是基于Cfglib的。在Spring的最底层,集成了这两种方法。
org.springframework.aop.framework.DefaultAopProxyFactory
。查看源码:
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
public DefaultAopProxyFactory() {
}
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
// 如果指定了 optimize为true 或者是proxyTargetClass 为true 或者是 没有实现接口
if (!config.isOptimize() && !config.isProxyTargetClass() &&
!this.hasNoUserSuppliedProxyInterfaces(config)) {
return new JdkDynamicAopProxy(config);
} else {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {// 目标类找不到 抛出异常
throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
} else {
// 目标类是接口 或者是 class是由代理类动态通过getProxyClass方法 或者 newProxyInstance方法生成 使用jdk 动态代理 否则 cglib 代理
return (AopProxy)(!targetClass.isInterface() &&
!Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
}
}
}
private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
Class<?>[] ifcs = config.getProxiedInterfaces();
return ifcs.length == 0 || ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0]);
}
}
通过上述源码可以发现,Spring生成代理对象的步骤:
注意:如果目标类没有实现接口,且class为final修饰的,则不能使用Spring AOP。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="accountService" class="com.atfwus.service.impl.ATServiceImpl">bean>
<bean id="logger" class="com.atfwus.utils.Logger">bean>
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.stfwus.service.impl.*.*(..))">aop:pointcut>
<aop:aspect id="logAdvice" ref="logger">
<aop:around method="aroundPringLog" pointcut-ref="pt1">aop:around>
aop:aspect>
aop:config>
beans>
package com.atfwus.service.impl;
import com.atfwus.service.IATService;
public class ATServiceImpl implements IATService{
@Override
public void save() {
System.out.println("保存信息");
// int i=1/0;
}
@Override
public void update(int i) {
System.out.println("更新信息"+i);
}
@Override
public int delete() {
System.out.println("删除信息");
return 0;
}
}
package com.atfwus.utils;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 记录日志
*/
public class Logger {
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("前置记录日志");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("后置记录日志");
return rtValue;
}catch (Throwable t){
System.out.println("异常日志记录");
throw new RuntimeException(t);
}finally {
System.out.println("最终日志记录");
}
}
}
import com.atfwus.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 测试AOP的配置
*/
public class AOPTest {
public static void main(String[] args) {
//1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.获取对象
IATService obj = (IATService)ac.getBean("ATService");
//3.执行方法
obj.save();
}
}
参考书籍:
- 《Spring实战》第四版
- 《SSM项目实战》
感谢耐心的您看到了这,谢谢您的支持!
ATFWUS --Writing By 2020–04-22