前言
学习重点:
- 什么是AOP
- AOP代理
- Spring通知
- AOP注解配置
1. Spring AOP简介
1.1 为什么需要使用AOP
- 业务方法中的日志、异常、事务的处理
- 在一个点上把涉及的公共问题集中的解决
Spring框架主要解决两个问题:
- 依赖注入(DI)
- 编写程序时不用关心其依赖的组件
- 面向方面/切面编程(AOP)
- 将程序中涉及的公共问题集中解决
1.2 什么是AOP
- AOP(Aspect-Oriented Programming,面向切面编程)
- AOP是OOP(面向对象编程)的补充和完善
- AOP的核心思想就是"将应用程序中的商业逻辑同对其提供支持的通用服务进行分离
例如:记录日志的代码是一个通用的服务,应该和具体的业务逻辑解耦,为所有的业务提供服务
java
public void doSameBusiness (long lParam,String sParam){
// 记录日志
log.info("调用 doSameBusiness方法,参数是:"+lParam);
// 输入合法性验证
if (lParam<=0){
throws new IllegalArgumentException("xx应该大于0");
}
try{
//真正的业务逻辑代码
//事务控制
}catch(){
tx.rollback();
}
// 事务控制
tx.commit();
}
1.3 AOP中的概念
- AOP的概念和术语
- 切面(Aspect) 事务处理,日志
- 连接点(Joinpoint) 方法调用,异常处理
- 通知(Advice) around,before,拦截器
- 切入点(Pointcut)匹配连接点的表达式
- 引入(Introduction)增加方法或者字段
- 目标对象(Target Object) 代理对象
- AOP代理(AOP Proxy) 框架产生的对象,包括Advice
- 织入(Weaving) Aspect连接到其他对象
2. AOP代理
2.1 为什么需要使用代理
为什么需要代理?在我看来,本质上还是为了解耦,我们将一些通用逻辑放在aop中,由aop生成代理对象来增强原有对象,原有对象只需要做好本职工作,其他由代理对象来完成,不仅利于后期代码的维护,也可以减少重复代码的书写量。
2.2 什么是AOP代理
- AOP代理是AOP框架创建的对象
- Spring有两种代理方式
- 默认使用JDK动态代理实现AOP代理,主要用于代理接口
- CGLIB代理,实现类的代理,而不是接口
2.3 手工实现代理方式
- 以手工的方式实现卖手机送充值卡的代理
电话接口:
java
public interface PhoneBiz {
void buyPhone(int num);//购买手机
void salePhone(int num) ;//销售手机
}
手机实现类:
java
//手机实现类
public class PhoneBizImpl implements PhoneBiz {
public void buyPhone(int num) {
System.out.println("手机进货,进货数量为:" + num + "部");
}
public void salePhone(int num) {
System.out.println("手机销售,销售数量为:" + num + "部");
}
}
日志操作类:
java
public class LogUtil {
public void log(String type,int num)
{
System.out.println("日志:"+currentTime()+type+"手机"+num+"部");
}
public String currentTime()
{
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
return sdf.format(new Date());
}
}
测试类:
java
@Test
public void test1()
{
PhoneBiz phoneBiz = new PhoneBizImpl();
phoneBiz.buyPhone(10);
phoneBiz.salePhone(5);
}
代理对象类:
java
public class PhoneBizImplProxy implements PhoneBiz {
private PhoneBiz phoneBiz=new PhoneBizImpl();// 目标对象
private LogUtil logUtil = new LogUtil();//日志对象
public void buyPhone(int num) {
phoneBiz.buyPhone(num);//调用目标对象的方法
logUtil.log("购买", num);//日志操作
}
public void salePhone(int num) {
phoneBiz.salePhone(num);//调用目标对象的方法
logUtil.log("销售", num);//日志操作
}
}
测试方法:
java
@Test
public void test2()
{
PhoneBiz phoneBiz = new PhoneBizImplProxy(); //创建代理对象
//调用代理对象的方法
phoneBiz.buyPhone(10);
phoneBiz.salePhone(5);
}
3. Spring对AOP的支持
- 首先引入AOP命名空间
XML
<beans xmlns=http://www.springframework.org/schema/beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
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-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
</beans>
这里演示的是spring3.0的配置
-
目标对象
- 包含业务关注点(需要日志管理的点)的对象
- PhoneBizImpl类的对象
<bean id="phoneBiz" class="service.PhoneBizImpl"></bean>
-
Spring的连接点(JoinPoint)
- 某个业务关注点
- 连接点总是代表某个方法的执行
- PhoneBizImpl类的buyPhone(int)方法和salePhone(int)的执行都是连接点
-
Spring的切入点
- 切入点是可以触发处理的连接点集合
- 同一个切入点可触发不同的处理
- 可以判断触发哪些处理
-
切入点、通知、横切关注点等在权限系统中的技术实现
3.1 Spring的切入点
- 切入点表达式
- 切入点指示符:如execution
- 布尔运算符:AND(&&)、OR(||)和NOT(!)
- 通配符:星号(*),用于匹配任何方法、类名或者参数类型。双点号(..),用于表示0个或者多个参数类型。
- 方法可见性修饰符:如public的。
- 方法返回类型:如void、int、String等*表示所有类型。
- 类名:指定完整的类名 ,缩小到某个目标类。
- 方法名称:可以是全名。如get*,即所有名称以get开头的方法。
- 方法声明抛出的异常:方法声明抛出的异常:
- 如 throws java.lang.IOException。
3.2 Spring的切面Aspect
-
系统中抽象出来的的某一个系统服务功能模块
- 日志管理模块
-
用一个POJO 类来表示抽象的切面,
-
用方法表示通知
*<bean id="logAspectBean" class="util.LogAspect"></bean
<aop:config> <aop:pointcut id="p1" expression="execution(void *Phone(int))"/> <aop:aspect id="logAspect" ref="logAspectBean"> <aop:before method="before" pointcut-ref="p1"/> </aop:aspect> </aop:config>
3.3 Spring的通知
- Spring中存在5种通知(advice)
- Before:在目标方法被调用之前调用(前置通知)
- AfterReturning:在某连接点正常完成后执行的通知
- After:当某连接点退出的时候执行的通知(最终通知)
- Throws:当目标方法抛出异常时调用(异常通知)
- Around:拦截对目标对象方法的调用(环绕通知)
3.4 Spring的前置通知
java
public void before(JoinPoint jp) throws Throwable{
Object[] args = jp.getArgs();// 目标方法所有参数
String methodname=jp.getSignature().getName();//获取目标方法名称
if("buyPhone".equals(methodname))
{
System.out.println("日志:"+currentTime()+"即将执行进货操作,数量为 "+args[0]+" 部");
}
if("salePhone".equals(methodname))
{
System.out.println("日志:"+currentTime()+"即将执行销售操作,数量为 "+args[0]+" 部");
}
}
- oinPoint对象提供了如下方法以获得连接点
- Object[] getArgs():返回方法参数
- Signature getSignature():返回方法签
- getModifiers()方法可以得到方法修饰符
- String getKind():返回当前连接点的类型
- Object getTarget():返回连接点所在的目标对象
- Object getThis():返回AOP自动创建的代理对象
- 前置通知的配置文件
XML
<aop:config>
<!-- 定义一个可以被多个切面共享的切入点 -->
<aop:pointcut id="p1" expression="execution( void *Phone(int))"/>
<!-- 定义一个切面 -->
<aop:aspect id="logAspect" ref="logAspectBean">
<!-- 定义一个前置通知 -->
<aop:before method="before" pointcut-ref="p1" />
</aop:aspect>
</aop:config>
- 测试方法
java
//创建代理对象
PhoneBiz pBiz = (PhoneBiz)ac.getBean("phoneBiz");
//购买100部手机
pBiz.buyPhone(100);
//销售88部手机
pBiz.salePhone(88);
System.out.println(pBiz.getClass().getName());
注意:
- 如果实现了至少一个接口,Spring会使用Java SE动态代理
- 注意,Java SE 动态代理只能针对接口产生代理
- 对于需要直接代理类而不是代理接口的时候,Spring也可以使用CGLIB代理。
- 只需要将<aop:config>标签的proxy-target-class属性置为"true"即可强制使用CGLIB针对类产生代理, <aop:config proxy-target-class="true">
3.5 Spring的后置通知
java
public void afterReturnnig(JoinPoint jp) throws Throwable{
String methodname=jp.getSignature().getName();//获取目标方法名称
if("buyPhone".equals(methodname))
{
System.out.println("日志:"+currentTime()+"进货操作执行完毕 ");
}
if("salePhone".equals(methodname))
{
System.out.println("日志:"+currentTime()+"销售操作执行完毕");
}
}
XML
<!-- 配置后置通知 -->
<aop:after-returning method="afterReturning" pointcut-ref="p1"/>
3.5 Spring异常通知
自定义异常类:
java
public class OutOfStockException extends Exception {
public OutOfStockException(String msg) {
super(msg);
}
}
java
public class PhoneBizImpl implements PhoneBiz {
int num;// 库存
public void buyPhone(int num) {
System.out.println("手机进货,进货数量为:" + num + "部");
this.num += num;
}
public void salePhone(int num) throws OutOfStockException {
if(this.num<num)
throw new OutOfStockException("存货不足,客户需要"
+ num+"部手机,库存只有"+this.num+"部");
System.out.println("手机销售,销售数量为:" + num + "部");
}
}
Spring异常通知的配置
java
public void afterThrowing(JoinPoint jp,OutOfStockException e) {
String methodName = jp.getSignature().getName();
System.out.println(currentTime()+methodName+
"方法执行,发生缺货异常"+e.getMessage());
}
XML
<!-- 配置异常通知 -->
<aop:after-throwing method="afterThrowing" pointcut-ref="p1" throwing="e" />
3.6 Spring最终通知
java
public void after(JoinPoint jp) throws Throwable {
String methodName = jp.getSignature().getName();
if ("buyPhone".equals(methodName)) {
System.out.println(currentTime() + "进货操作执行完毕,发生异常也要执行的最终通知...");
}
if ("salePhone".equals(methodName)) {
System.out.println(currentTime() + "销售操作执行完毕,发生异常也要执行的最终通知...");
}
}
XML
<!--配置最终通知 -->
<aop:after method="after" pointcut-ref="p1"/>
3.7 Spring的环绕通知
java
public Object aroundTest(ProceedingJoinPoint pjp) throws Throwable {
String method = pjp.getSignature().getName();
long begin = System.currentTimeMillis();
System.out.println(currentTime()+":"+method+"方法开始执行,计时开始!");
try {
return pjp.proceed();
} finally{
long end = System.currentTimeMillis();
System.out.println(currentTime()+":"+method+"方法执行完毕,耗时"
+(end-begin)+ "毫秒");
}
}
XML
<!--配置环绕通知 -->
<aop:around method="aroundTest" pointcut-ref="p1"/>
Spring注解式通知
XML
<!-- 启用注解配置 -->
<aop:aspectj-autoproxy />
<!-- 目标业务对象 -->
<bean id="phoneBiz" class="s3spring.ch2.biz.impl.PhoneBizImpl"></bean>
<!-- 日志管理切面 -->
<bean class="s3spring.ch2.log.annotation.LogAspect"></bean>
- @Aspect注解声明为切面
- package s3spring.ch2.log.annotation; @Aspect public class LogAspect {......}
- spring提供了如下注解来配置通知:
- @Before,前置通知
- @AfterReturning,后置通知
- @AfterThrowing,异常通知
- @After,最终通知
- @Around,环绕通知
- 为切面添加注解
java
@Aspect
public class LogAspect {
/** 前置通知 在目标方法执行之前执行日志记录 */
@Before("execution( void *Phone(int))")
public void before(JoinPoint jp) throws Throwable { ...... }
/** 后置通知 在目标方法正常退出时执行日志记录 */
@AfterReturning("execution( void *Phone(int))")
public void afterReturning(JoinPoint jp) throws Throwable { ...... }
/** 最终通知 无论目标方法正常退出还是异常退出都执行日志记录 */
@After("execution( void *Phone(int))")
public void after(JoinPoint jp) throws Throwable {...... }
/** 环绕通知 */
@Around("execution( void *Phone(int))")
public void after(JoinPoint jp) throws Throwable {...... }
......省略异常通知
}
- @AfterThrowing注解配置异常通知除了需要指定切入点外还需要根据方法参数名称绑定异常对象
java
@Aspect
public class LogAspect {
......
/** 异常通知 在目标方法抛出参数指定类型异常时执行 */
@AfterThrowing(pointcut="execution( void *Phone(int))",throwing="e")
public void afterThrowing(JoinPoint jp,OutOfStockException e) {
......
}
}
- @Pointcut注解结合切入点表达式在LogAspect类中定义一个切入点
java
@Aspect
public class LogAspect {
/** 切入点 */
@Pointcut("execution( void *Phone(int))")
public void p1(){}
/** 前置通知 在目标方法执行之前执行日志记录 */
@Before("p1()")
public void before(JoinPoint jp) throws Throwable { ...... }
}
总结
- AOP
- Spring AOP
- AOP的代理
- Spring通知