承上:
SpringAOP的基本认识
先从AOP前置通知类型为例,写一个简单的demo,从入门级了解AOP的使用。
IDE环境:Intellj IDEA
步骤:
1先建立一个Spring项目。项目结构如下:
已经知道AOP四要素
方面组件————-LogUtil类(添加日志功能)
目标组件————-EntityDao类(进行数据库操作的功能)
EntityDao类代码:(因为主要学习AOP,所以数据库的操作方法简单写一下就行)
package com.hnust.service;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Service;
@Service
public class EntityDao {
public void save(){
System.out.println("保存操作");
}
public void update(){
System.out.println("更新操作");
}
public void delete(){
System.out.println("删除操作");
}
public static void main(String[] args){
//获取Spring容器
ApplicationContext atc = new ClassPathXmlApplicationContext("spring-config.xml");
//通过Spring容器创建EntityDao对象
EntityDao ed = atc.getBean(EntityDao.class);
System.out.println("目标组件"+ed.getClass().getName());
ed.save();
ed.update();
ed.delete();
}
}
Spring配置文件spring-config.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.hnust"></context:component-scan>
</beans>
这里涉及IOC注解注入和组件扫描的知识,先不讲解(因为我也还没学0.0),知道就是添加了这个注解,通过组件扫描,就可以将对象添加进入Spring容器中,交由Spring管理,这是Spring容器利用IOC创建对象的一种方式就行。
我们可以运行代码,得到一下结果
现在已经模拟完成数据库的增删改操作啦。
为了增加数据库的安全性,我们现在对数据库每一个操作之前添加日志文件,也就是在目标组件中添加方面组件模块,而且目标组件中的方法有格式要求,方法必须是共有的,无返回值,至于带不带参数,取决于通知类型,我们先使用前置通知类型进行测试,不需要带参数。
LogUtil类代码:
package com.hnust.aspect;
import org.springframework.stereotype.Component;
@Component
public class LogUtil {
public void log1(){
System.out.println("**数据库操作日志已记录**");
}
}
有了目标组件和方面组件,AOP四要素还缺两个,就是通知和切入点,这两个都在Spring配置文件中配置就行。
在Spring配置文件spring-config.xml文件添加AOP配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.hnust"></context:component-scan>
<!--AOP配置-->
<aop:config>
<!--声明方面组件-->
<aop:aspect ref="logUtil">
<aop:before method="log1"
pointcut="within(com.hnust.service.*)"/>
</aop:aspect>
</aop:config>
</beans>
我们学习一下配置规则。
配置之后,再运行目标组件,正常情况结果是这样
可以看到,AOP的配置使得目标组件成功调用方面组件的功能。
需要注意的是,Intellij IDEA中创建Spring项目时,包没有导全,我们在使用AOP功能时,需要导入aspectjweaver-1.8.10.jar,如果使用maven构建项目,那么在maven配置文件中添加该包的依赖,如果没有使用maven,那么就下载这个包,添加进项目的lib文件中就行(坑呀,这个问题花了大半天没有解决,最后还是仔细看错误提示才找到原因,提醒各位出现BUG时习惯先看错误输出,不要无脑去看源程序)。
AOP的入门使用大概就是上面讲解的了,我们知道AOP使得程序更加灵活,之间就讲过,AOP可以使得方面组件可以植入目标组件的任意位置,也就是说目标组件哪些方法可以调用方面组件,什么时机可以调用方面组件,所以,我们可以通过修改AOP配置中的切入点和通知去改变。
下面介绍AOP通知类型:
- 前置通知:调用目标组件前,调用方面组件(before)
- 后置通知:调用目标组件后,调用方面组件(after-returning)
- 最终通知:调用目标组件后,在finally里调用方面组件(after)
- 异常通知:目标组件发生异常时,调用方面组件。(after-throwing)
-
环绕通知:调用目标组件前、后,分别调用一次方面组件。(around)
AOP就是按照这五种通知类型进行分类的。
用代码可以更清晰的明白各通知之间的位置,我们关注点不在各通知之间的相对顺序,只要关注某个通知类型和关键业务代码的顺序就行了。
try{
try{
//环绕通知-前置
//前置通知
ed.save();
//环绕通知-后置
}(Exception e){
throw e;
}finally{
//最终通知
}
//后置通知
}catch(Exception e){
//异常通知
}
我们利用上面的Demo来测试一下这五种类型的通知吧,了解不同类型通知的差异。
前面Demo使用的是前置通知,后置通知和最终通知是一类,因为方面组件方法是无参的。只需要在配置文件中改变通知类型就可以。
将before关键字依次改为after-returning、after,分别是后置通知和最终通知,运行结果一样,如下图:
那么后置通知和最终通知区别是什么呢?学过异常处理机制就很容易理解,如果目标组件中业务核心代码发生出错,那么后置通知不会执行,但是最终通知会执行,因为最终通知位于finally模块中。
那么异常通知类型呢,我们需要在方面组件的方法中加入异常处理的参数。
增加一个处理异常的方法log2
package com.hnust.aspect;
import org.springframework.stereotype.Component;
@Component
public class LogUtil {
public void log1(){
System.out.println("**数据库操作日志已记录**");
}
public void log2(Exception e){
System.out.println("调用异常通知日志");
//得到异常数组并输出
String error = e.getStackTrace()[0].toString();
System.out.println(error);
}
}
在配置文件中配置异常通知类型
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.hnust"></context:component-scan>
<!--AOP配置-->
<aop:config>
<!--声明方面组件-->
<aop:aspect ref="logUtil">
<!--前置、后置、最终通知-->
<aop:after-returning method="log1"
pointcut="within(com.hnust.service.*)"/>
<!--异常通知-->
<aop:after-throwing method="log2" throwing="e"
pointcut="within(com.hnust.service.*)"/>
</aop:aspect>
</aop:config>
</beans>
此时运行目标组件,发现异常通知并没有输出,因为目标组件中没有方法出现异常,所以方面组件中的异常方法不会触发,我们可以在目标组件某个方法中加入异常代码,可以看到输出异常信息
那么这个异常机制是怎么实现的呢?我们需要先了解一下AOP原理(感觉有点乱了,随意吧):
AOP实现原理使用的是动态代理的技术,可以在程序运行阶段,根据业务的要求在内存中创建代理类,Spring采用2中方式实现动态代理:
1:CGLIB工具包
动态创建目标的子类作为代理类,所有的类都可以用它来实现代理
2:JDK Proxy API
动态创建接口的实现类作为代理类,带有接口的类可以由它实现代理
可以知道后者有一定要求。
那么Spring如何使用动态代理技术实现AOP这种功能呢?
一张图可以明白
我们可以通过以上的实验结论验证
当没有使用AOP配置时:
使用了AOP配置后:
在目标组件中有这样一行代码
System.out.println("目标组件"+ed.getClass().getName());
这是输出目标组件的类名,调用AOP之后目标组件的类名便是代理类的类名,代理类是继承EntityDao的一个子类。
根据AOP的通知类型可以使得调用方面组件和super.save()方法顺序可变。
知道了AOP的动态代理技术,了解AOP的异常通知类型也就容易了。
在代理类中处理异常的机制是这样
try{
super.save();
}catch(Exception e){
logUtil.log2(e);
}
当目标组件发生异常时,Sping 会将异常传给方面组件,然后在方面组件方法内部处理异常和输出异常日志。
最后介绍环绕通知的写法
增加一个处理环绕通知的方法log3
package com.hnust.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
@Component
public class LogUtil {
public void log1(){
System.out.println("**数据库操作日志已记录**");
}
public void log2(Exception e){
System.out.println("调用异常通知日志");
//得到异常数组并输出
String error = e.getStackTrace()[0].toString();
System.out.println(error);
}
public Object log3(ProceedingJoinPoint pj)throws Throwable{
System.out.println("调用log3()记录日志-环绕通知前置");
Object obj=pj.proceed();
System.out.println("调用log3()记录日志-环绕通知后置");
return obj;
}
}
在配置文件中配置环绕通知类型
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.hnust"></context:component-scan>
<!--AOP配置-->
<aop:config>
<!--声明方面组件-->
<aop:aspect ref="logUtil">
<!--前置、后置、最终通知-->
<aop:after-returning method="log1"
pointcut="within(com.hnust.service.*)"/>
<!--异常通知-->
<aop:after-throwing method="log2" throwing="e"
pointcut="within(com.hnust.service.*)"/>
<!--环绕通知-->
<aop:around method="log3"
pointcut="within(com.hnust.service.*)"/>
</aop:aspect>
</aop:config>
</beans>
运行后:
环绕通知的原理:
代理对象在复写目标组件方法中做了两件事:
1.目标组件方法new了一个ProcessdingJoinPoint()对象,同时初始化目标组件对象,放到ProcessdingJoinPoint()对象中。
2.logUtil.log3(p)。
LogUtil类中log3方法怎么处理的呢?
前置内容
p.proceed()—-执行目标方法
后置内容