理解面向切面编程
AOP(Aspect Oriented Programming)面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术,是面向对象编程的一种延续与补充。
用处
专门用于处理系统中分布在各个模块(不同方法)中的交叉关注点。
具体体现以及优点
在代理模式中提到过,核心业务与非核心业务分离时,在改变非核心业务逻辑时,并不会影响核心业务逻辑,使得两种业务逻辑之间的耦合度降低,提高程序的可重用性和可维护性。
而通过AOP就可以将分离的核心业务与非核心业务结合起来,从而实现对类的增强。将核心业务(方法)切开,将非核心业务加入(织入)进入
理解AOP中的几个名词
在实现SpringAOP之后再来理解这些名词,会更加深入
JoinPoint 连接点:实际上为一个类,封装了SpringAop中代理类实例和被代理类实例的信息。用于将核心业务方法和增强方法连接起来
PointCut 切入点:横向交织的共同逻辑,也就是定义切入的规则,从哪里开始切入。使用aspect表达式语言定义规则,需要导入jar包
Target 目标对象:代理模式中的的真实角色,在此不作赘述
Proxy 代理对象:代理模式中的的代理角色,在此不作赘述
Advice 通知:在程序中表现为方法,实现非核心业务逻辑,对核心业务逻辑增强的代码
根据通知的位置(参照物是核心业务逻辑),可以分为前置通知、后置通知、环绕通知、异常通知、最终通知
Weaving 织入:可以理解为代理对象中的各种通知
Aspect 切面:切开物体时,会产生两个切面,在程序中切开方法时,也抽象的理解为产生了两个切面,而具体体现为一个类
xml文件实现SpringAOP
实现的代码
UserInfoDao类
import com.young.bean.UserInfo;
import org.springframework.stereotype.Repository;
@Repository
public class UserInfoDao implements IUserInfoDao {
@Override
public void save(UserInfo ui) {
System.out.println("UserInfoDao save method invoke");
}
}
UserInfoService类
import com.young.bean.UserInfo;
import com.young.dao.IUserInfoDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public final class UserInfoService {
@Autowired
private IUserInfoDao userInfoDao;
public void register(UserInfo ui) {
userInfoDao.save(ui);
}
}
首先我们需要创建类作为切面类
该类的作用就是实现非核心业务逻辑,但是现在该类只是一个普通类,需要在xml配置文件中配置
public class LoggerAspect {
public void before(){
System.out.println("LoggerAspect before method invoke");
}
}
配置文件
<context:component-scan base-package="com.young"/>
<!--将LoggerAspect类交给Spring容器-->
<bean id="loggerBean" class="com.young.aspect.LoggerAspect"/>
<!--我们需要关注并解释以下代码-->
<aop:config>
<aop:aspect id="loggerAspect" ref="loggerBean">
<aop:before method="before" pointcut="execution(public void com.young.service.UserInfoService.register(..))"/>
</aop:aspect>
</aop:config>
配置文件解释:
<aop:config>是AOP的配置标签
通过设置<aop:aspect>标签的属性以及子标签来配置切面类
<aop:aspect id="loggerAspect" ref="loggerBean">
id属性是设置切面类的id值,ref是设置切面类的引用,将id为loggerBean的bean设置为切面类。
<aop:before>标签用于配置前置通知(后文详细写通知)
method属性是指定作为前置通知的方法名
pointcut属性是设置切入点,需要使用aspect表达式,常用execution()表达式
execution(访问修饰符 返回值 包.类.方法(参数)),参数为方便通常使用"..",代表各种参数的方法
测试方法:
@Test
public void testAOP(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserInfoService userInfoService = context.getBean("userInfoService", UserInfoService.class);
userInfoService.register(new UserInfo());
}
运行结果:
LoggerAspect before method invoke
UserInfoDao save method invoke
探究从Spring容器中获取的对象类型
在<aop:aspect>标签下配置如下内容
<aop:before method="before" pointcut="execution(public void com.young.dao.UserInfoDao.save(..))"/>
UserInfoDao类实现了接口,而UserInfoService类没有实现接口,在测试类中获取这两个对实例的类名
@Test
public void testAOP(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserInfoService userInfoService = context.getBean("userInfoService", UserInfoService.class);
System.out.println(userInfoService.getClass().getName());
IUserInfoDao userInfoDao = context.getBean("userInfoDao", IUserInfoDao.class);
//BeanNotOfRequiredTypeException
System.out.println(userInfoDao.getClass().getName());
}
测试方法运行结果:
com.young.service.UserInfoService$$EnhancerBySpringCGLIB$$afb43664
com.sun.proxy.$Proxy13
根据输出结果可得:
如果类实现了接口,调用getBean方法时,Spring容器会使用jdk代理方式得到一个代理类实例返回给客户端,所以需要使用接口来接收这个对象。
如果类没有实现接口,调用getBean方法时,Spring容器会使用CGLib代理方式得到一个代理类实例返回给客户端。
JoinPoint连接点
JoinPoint对象封装了SpringAop中代理类实例和被代理类实例的信息,在通知中添加JoinPoint参数
可以在通知方法中通过该对象调用方法
Signature getSignature()
获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
Object[] getArgs()
获取传入目标方法的参数对象
Object getTarget()
获取被代理的对象
Object getThis()
获取代理对象
LoggerAspect切面类
public class LoggerAspect {
public void before(JoinPoint joinPoint){
String mname = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
String cname = joinPoint.getTarget().getClass().getName();
String str=String.format("类名:%s 方法名:%s 参数:%s",cname,mname, Arrays.toString(args));
System.out.println(str);
System.out.println("LoggerAspect before method invoke");
}
}
测试方法
@Test
public void testAOP(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserInfoService userInfoService = context.getBean("userInfoService", UserInfoService.class);
userInfoService.register(new UserInfo());
}
测试结果:
类名:com.young.service.UserInfoService 方法名:register 参数:[com.young.bean.UserInfo@649bec2e]
LoggerAspect before method invoke
UserInfoDao save method invoke
ProceedingJoinPoint类是JoinPoint类的子类,它新增了两个用于调用目标方法的方法
java.lang.Object proceed() throws java.lang.Throwable
通过反射执行目标对象的连接点处的方法;
java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable
通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。
该类在around(环绕通知)时使用,代码如下:
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("LoggerAspect around start");
Object result = proceedingJoinPoint.proceed();
System.out.println("LoggerAspect around end");
return result;
}
在以上代码中,proceed方法用于执行目标方法,如果不调用该方法,后续代码不会执行。
有的目标方法存在返回值,所以需要返回一个结果,类型为Object
调用该方法之前的代码属于环绕通知的前置内容,在目标方法前执行
调用该方法之后的代码属于环绕通知的后置内容,在目标方法后执行
Advice通知
根据通知的位置对通知进行如下分类
前置通知 before:在我们执行目标方法之前运行
后置(最终)通知 after:在我们目标方法运行结束之后 ,不管有没有异常
返回通知 after-returning:在我们的目标方法正常返回值后运行
异常通知 after-throwing:在我们的目标方法出现异常后运行
环绕通知 around:可以指定执行目标方法之前和之后执行的代码,相当于前置通知+后置通知
接下来可以通过代码以及测试方法理解各种通知的区别
配置文件:
<aop:config>
<aop:aspect id="loggerAsp" ref="loggerBean">
<!--可以通过<aop:pointcut>标签配置切点,然后在通知标签中使用pointcut-ref属性来引用该切点-->
<!--execution()表达式中,通常使用通配符,第一个"*"代表所有返回值,修饰符可以省略,之后代表com.young包下的所有包的所有类所有方法-->
<aop:pointcut id="loggerPointcut" expression="execution(* com.young.*.*.*(..))"/>
<aop:before method="before" pointcut-ref="loggerPointcut"/>
<aop:after-returning method="afterReturning" pointcut-ref="loggerPointcut"/>
<aop:after method="after" pointcut-ref="loggerPointcut"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="loggerPointcut"/>
<aop:around method="around" pointcut-ref="loggerPointcut"/>
</aop:aspect>
</aop:config>
切面类LoggerAspect
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
public class LoggerAspect {
public void before(JoinPoint joinPoint){
System.out.println("LoggerAspect before method invoke");
}
public void afterReturning(){
System.out.println("LoggerAspect afterReturning method invoke");
}
public void after(){
System.out.println("LoggerAspect after method invoke");
}
public void afterThrowing(){
System.out.println("LoggerAspect afterThrowing method invoke");
}
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("LoggerAspect around start");
Object result = proceedingJoinPoint.proceed();
System.out.println("LoggerAspect around end");
return result;
}
}
测试方法
@Test
public void testAOP(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
IUserInfoDao userInfoDao = context.getBean("userInfoDao", IUserInfoDao.class);
userInfoDao.save(new UserInfo());
}
测试方法运行结果:
LoggerAspect before method invoke
LoggerAspect around start
UserInfoDao save method invoke
LoggerAspect around end
LoggerAspect after method invoke
LoggerAspect afterReturning method invoke
在save方法中手动制造异常
@Repository
public class UserInfoDao implements IUserInfoDao {
@Override
public void save(UserInfo ui) {
System.out.println("UserInfoDao save method invoke");
int num=1/0;
}
}
再次运行测试方法,结果如下:
LoggerAspect before method invoke
LoggerAspect around start
UserInfoDao save method invoke
LoggerAspect afterThrowing method invoke
LoggerAspect after method invoke
在异常通知中我们可以获取异常对象作为参数
public void afterThrowing(Throwable t){
System.out.println("LoggerAspect afterThrowing method invoke "+t.getMessage());
}
需要注意的是,如果在通知中获取了异常对象,在配置文件中需要设置属性throwing
<aop:after-throwing method="afterThrowing" throwing="t" pointcut-ref="loggerPointcut"/>
细节注意:
1.前置通知与环绕通知的执行先后问题
在xml文件实现的方式中,执行先后与配置文件中的顺序有关。
在以上的配置文件中<aop:before>标签在<aop:around>标签之前,所以前置通知会先执行。
如果将<aop:around>标签放在前面,那么环绕通知会先执行。
2.返回通知与后置通知执行先后问题
执行先后也与配置文件中的顺序有关
如果<aop:after-returning>标签在前,那么返回方法(after-returning)后执行
反之如果<aop:after>标签在前,那么后置方法(afte)后执行
顺序问题总结:
配置文件中的顺序不同,可能执行的顺序就不一样,具体情况不止这两种,如果测试时出现顺序不一致,多半就是配置文件顺序问题。
3.在环绕通知中的异常不要进行try-catch处理
因为proceed方法是执行目标方法的,如果进行了try-catch处理,目标方法出现异常时,
会导致Spring容器不知道具体发生了什么异常,也就不会调用异常通知中的方法。
Advice的使用场景
环绕通知、异常通知常应用于事务控制
返回通知、最终通知常应用于日志处理
当然需要根据实际业务需求来选择不同的通知,没有具体的规范
使用通知器配置
切面类LoggerAspect2
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
//在切面类中通过实现接口,重写方法来指定通知的类型
public class LoggerAspect2 implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("..."+method.getName()+"...");
}
}
配置文件
<bean id="loggerBean2" class="com.young.aspect.LoggerAspect2"/>
<aop:config>
<!--定义通知器-->
<aop:advisor advice-ref="loggerBean2" pointcut="execution(* com.young.*.*.*(..))"/>
</aop:config>
<aop:advisor>大多用于事务管理。
<aop:aspect>大多用于日志、缓存。