接着上一篇文章的内容Spring框架完全掌握(上),我们继续深入了解Spring框架。

Spring_AOP

考虑到AOP在Spring中是非常重要的,很有必要拿出来单独说一说。所以本篇文章基本上讲述的就是关于Spring的AOP编程。

简介

先看一个例子:

package com.itcast.spring.bean.calc;

public class ArithmeticCalculatorImpl implements ArithmeticCalculator {

	@Override
public int add(int num1, int num2) {
int result = num1 + num2;
return result;
} @Override
public int sub(int num1, int num2) {
int result = num1 - num2;
return result;
} @Override
public int mul(int num1, int num2) {
int result = num1 * num2;
return result;
} @Override
public int div(int num1, int num2) {
int result = num1 / num2;
return result;
}
}

这是一个实现四则运算接口的实现类,能够进行两个数之间的加减乘除。而这个时候,我们有一个需求,就是在每个方法执行前后都必须输出日志信息,那么我们就得在每个方法中都加上日志信息:

...
@Override
public int add(int num1, int num2) {
System.out.println("add method start with[" + num1 + "," + num2 + "]");
int result = num1 + num2;
System.out.println("add method start with[" + num1 + "," + num2 + "]");
return result;
}
...

这样所带来的问题是什么呢?

  1. 代码混乱:越来越多的非业务需求(例如日志、参数验证等)加入后,原有的业务方法急剧膨胀,每个方法在处理核心逻辑的同时还必须兼顾其它多个关注点。
  2. 代码分散:以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块里多次重复相同的日志代码,如果日志需求发生变化,必须修改所有模块中的日志代码。

既然问题出现了,该如何解决呢?(使用动态代理)

public class ArithmeticCalculatorLoggingProxy {

	private ArithmeticCalculator target;

	public ArithmeticCalculator getLoggingProxy() {
ArithmeticCalculator proxy = null; ClassLoader loader = target.getClass().getClassLoader();
Class[] interfaces = new Class[] { ArithmeticCalculator.class };
InvocationHandler h = new InvocationHandler() { @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "method start with[ " + Arrays.asList(args) + "]");
Object result = method.invoke(target, args);
System.out.println(method.getName() + "method end with[ " + result + "]");
return result;
}
};
proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader, interfaces, h);
return proxy;
}
}

这样我们就可以去获取代理对象从而实现日志业务却不改变基本业务代码。

其实这样实现还是略显麻烦,但不用担心,Spring框架为我们提供了一种实现方式——AOP。

AOP(Aspect-Oriented Programming,面向切面编程):这是一种新的方法论,是对传统OOP(Object-Oriented Programming,面向对象编程)的补充,AOP的主要编程对象是切面。

在应用AOP编程时,仍然需要定义公共功能,但可以明确地定义这个功能在哪里,以什么方式应用,并且不必修改受影响的类,这样一来,横切关注点就被模块化到特殊的对象里。

好处:

  1. 每个事物逻辑位于一个位置,代码不分散,便于维护和升级
  2. 业务模块更简洁,只包含核心业务代码

这样来看,AOP能够非常精准地解决我们遇到了问题。

前置通知

在Spring中,可以使用基于AspectJ注解或基于XML配置的AOP。AspectJ是Java社区里最完整最流行的AOP框架,所以我们以AspectJ注解方式为例进行讲解。

首先导入AOP框架的jar包:



然后我们在上面的案例中进行修改:

@Component
public class ArithmeticCalculatorImpl implements ArithmeticCalculator { @Override
public int add(int num1, int num2) {
int result = num1 + num2;
return result;
} @Override
public int sub(int num1, int num2) {
int result = num1 - num2;
return result; } @Override
public int mul(int num1, int num2) {
int result = num1 * num2;
return result;
} @Override
public int div(int num1, int num2) {
int result = num1 / num2;
return result;
}
}

这里在实现类的开头加上了一个注解,目的是将该类交由Spring容器管理,其它代码不作改动。

//将该类声明为一个切面
@Aspect
@Component
public class LoggingAspect { // 声明该方法是一个前置通知:在目标方法开始之前执行
@Before("execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.add(int,int))")
public void beforeMethd(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName + " method start with" + args);
}
}

接着我们将输出日志的业务看成一个切面,创建一个类,然后任意地定义一个方法,该方法要添加一个注解:Before。用于声明该方法是一个前置通知,前置通知方法会在目标方法开始之前执行。所以我们还需要在Before中声明目标方法。该方法可以添加一个参数为JoinPoint类型,执行方法的方法名和参数都封装在该对象中。其次,该类必须也交由Spring容器管理,所以添加注解@Component,且该类为一个切面,添加注解@Aspect。

然后要在配置文件中进行配置:

<?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-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd"> <!-- 配置自动扫描的包 -->
<context:component-scan
base-package="com.itcast.aop.impl"></context:component-scan> <!-- 使AspjectJ注解起作用:自动为匹配的类生成代理对象 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

这样,框架会去自动寻找匹配的类并生成代理对象。

最后编写测试代码:

public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class);
int result = ac.add(1, 1);
System.out.println("result:" + result);
}

运行结果:

add method start with[1, 1]
result:2

但是当你调用其它的运算方法时发现日志信息又无法打印了,这是因为你在配置目标方法的时候配置的仅仅是add()方法,所以可以采用通配符的方式将类中的所有方法都配置进去。

@Before("execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))")

这里的exeution是执行的意思,也就是说,该属性的括号内填写的是目标方法,对于该目标方法,可以更加抽象地进行表示,例如权限修饰符、返回值等等都可以用通配符进行替换。

到这里,SpringAOP就轻松实现了我们开始遇到的问题。

后置通知

既然有前置通知,那肯定就会有后置通知,后置通知的实现方式和前置通知类似:

@After("execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))")
public void afterMetohd(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName + " method ends with" + args);
}

运行测试代码,结果如下:

add method start with[1, 1]
add method ends with[1, 1]
result:2

后置通知是在目标方法执行后执行,但需要注意的是,后置通知不管目标方法是否成功执行,就算目标方法在执行过程中产生了异常,后置通知仍然会执行,而且在后置通知中无法访问到目标方法的执行结果。

返回通知

返回通知和后置通知类似,但是返回通知只在目标方法正确执行完成后才执行,如果目标方法在执行过程中产生了错误,返回通知将不起作用。所以返回通知能够获取目标方法的执行结果:

	// 声明该方法是一个返回通知:在方法正常执行结束后执行
// 返回通知是可以访问到目标方法的返回值的
@AfterReturning(value = "execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName + " method ends with" + result);
}

运行结果:

add method start with[1, 1]
add method ends with[1, 1]
add method ends with2
result:2

异常通知

异常通知是在目标方法执行过程中产生了异常后才会执行,异常通知能够获取到目标方法产生的异常信息:

	// 声明该方法是一个异常通知:在方法执行产生异常时执行
// 异常通知可以获取到产生的异常信息
@AfterThrowing(value = "execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println(methodName + " method's exception is " + ex);
}

我们人为产生一个异常来测试一下:

public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class); result = ac.div(10, 0);
System.out.println("result:" + result);
}

运行结果:

div method start with[10, 0]
div method ends with[10, 0]
div method's exception is java.lang.ArithmeticException: / by zero

环绕通知

对于环绕通知,这在所有通知中是功能最强大的通知,其实它并不常用,但是我们还是得了解一下它的用法:

	// 声明该方法是一个环绕通知,环绕通知需要携带ProceedingJoinPoint类型的参数
// 环绕通知类似于动态代理的全过程
// ProceedingJoinPoint类型的参数可以决定是否执行目标方法
// 且环绕通知必须有返回值,返回的是目标方法的返回值
@Around(value = "execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))")
public Object aroundMethod(ProceedingJoinPoint point) {
Object result = null;
String methodName = point.getSignature().getName();
// 执行目标方法
try {
// 前置通知
System.out.println(methodName + " method' start with" + Arrays.asList(point.getArgs()));
result = point.proceed();
// 返回通知
System.out.println(methodName + " method' end with " + result);
} catch (Throwable e) {
// 异常通知
System.out.println(methodName + " method's exception is " + e);
}
// 后置通知
System.out.println(methodName + " method' end with");
return result;
}

环绕通知能够实现其它所有通知的功能,但是它有很多限制。

  • 必须要携带ProceedingJoinPoint类型的参数
  • 环绕通知必须有返回值,返回的是目标方法的返回值

测试代码:

public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class);
int result = ac.add(1, 1);
System.out.println("result:" + result);
}

运行结果:

add method' start with[1, 1]
add method' end with 2
add method' end with
result:2

切面的优先级

在具有多个切面的项目中,我们可以指定切面的优先级,决定切面的先后执行顺序。使用@Order()注解来配置优先级(在类开头注解),括号里填入一个整数,值越小优先级越高。

例如:

@Order(1)
public class LoggingAspect {
......
......
}

关于SpringAOP的相关内容就说到这里,如有错误,欢迎指正。

Spring框架完全掌握(下)的更多相关文章

  1. Spring框架基础(下)

    log4J 导入log4J.jar 创建log4J.properties # Create a file called log4j.properties as shown below and plac ...

  2. Spring框架下的定时任务quartz框架的使用

    手头的这个项目需要用到定时任务,但之前没接触过这东西,所以不太会用,从网上找资料,大致了解了一下,其实也不难.Java的定时任务实现有三种,一种是使用JDK自带的Timer那个类来实现,另一种是使用q ...

  3. 手撸Spring框架,设计与实现资源加载器,从Spring.xml解析和注册Bean对象

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 你写的代码,能接的住产品加需求吗? 接,是能接的,接几次也行,哪怕就一个类一片的 i ...

  4. 最新 Eclipse IDE下的Spring框架配置及简单实例

    前段时间开始着手学习Spring框架,又是买书又是看视频找教程的,可是鲜有介绍如何配置Spring+Eclipse的方法,现在将我的成功经验分享给大家. 本文的一些源代码来源于码农教程:http:// ...

  5. Spring 框架下 (增 删 改 )基本操作

    //applicationContext.xml 配置文件 <?xml version="1.0" encoding="UTF-8"?><be ...

  6. Eclipse IDE下的Spring框架使用简单实例

    Eclipse IDE下的Spring框架使用简单实例 1 准备Java jdk安装. Eclipse软件安装.根据系统安装32/64版本,选择Eclipse IDE for Java Develop ...

  7. Spring框架下Junit测试

    Spring框架下Junit测试 一.设置 1.1 目录 设置源码目录和测试目录,这样在设置产生测试方法时,会统一放到一个目录,如果没有设置测试目录,则不会产生测试代码. 1.2 增加配置文件 Res ...

  8. Spring框架找不到 applicationContext.xml文件,可能是由于applicationContext.xml文件的路径没有放在根目录下造成的

    Spring框架找不到 applicationContext.xml文件,可能是由于applicationContext.xml文件的路径没有放在根目录下造成的

  9. 深入剖析 RabbitMQ —— Spring 框架下实现 AMQP 高级消息队列协议

    前言 消息队列在现今数据量超大,并发量超高的系统中是十分常用的.本文将会对现时最常用到的几款消息队列框架 ActiveMQ.RabbitMQ.Kafka 进行分析对比.详细介绍 RabbitMQ 在 ...

随机推荐

  1. restTemplate重定向问题 &cookie问题

    最近在做一个转发功能,zuul + ribbon + resttemplate 进行路由.负载.转发的功能 基本准备就绪,在微信自动登陆那遇到了一个坑,ribbon 系统用resttemplate 转 ...

  2. C#3.0新增功能09 LINQ 基础06 LINQ 查询操作中的类型关系

    连载目录    [已更新最新开发文章,点击查看详细] 若要有效编写查询,应了解完整的查询操作中的变量类型是如何全部彼此关联的. 如果了解这些关系,就能够更容易地理解文档中的 LINQ 示例和代码示例. ...

  3. [小米OJ] 10. 爬楼梯

    dp 另: 小米oj上的测试样例是错的 ; ) function solution(line) { if (line == 0) return 0; if (line == 1) return 1; ...

  4. Java 读写 excel 实战完全解析

    本文微信公众号「AndroidTraveler」首发. 背景 时值毕业季,很多毕业生初入职场. 因此,这边也写了一些新手相关的 Android 技术点. 比如上一篇的 Android 开发你需要了解的 ...

  5. Java中返回值定义为int类型的 方法return 1返回的是int还是Integer&&finally中return问题

    在Java中返回值定义为int类型的 方法return 1:中返回的是Integer值,在返回的时候基本类型值1被封装为Integer类型. 定义一个Test类,在异常处理try中和finally中分 ...

  6. JavaScript的面向对象原理之原型链

    二.JavaScript的对象 为了能够清楚的解释这一切,我先从对象讲起.从其他面向对象语言(如Java)而来的人可能认为在JS里的对象也是由类来实例化出来的,并且是由属性和方法组成的. 实际上在JS ...

  7. springboot-rabbitMQ

    作者:纯洁的微笑出处:http://www.ityouknow.com/ 版权所有,欢迎保留原文链接进行转载:) RabbitMQ 即一个消息队列,主要是用来实现应用程序的异步和解耦,同时也能起到消息 ...

  8. chapter02 - 03

    1.分别用cat \tac\nl三个命令查看文件/etc/ssh/sshd_config文件中的内容,并用自己的话总计出这三个文档操作命令的不同之处? 答:cat /etc/ssh/sshd_conf ...

  9. RobotFramework_4.SeleniumLibrary操作(二)

    *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...

  10. 【iOS】iOS Error Domain=NSCocoaErrorDomain Code=3840 "未能完成操作。(“Cocoa”错误 3840。)"

    昨天遇到的这个问题,详细信息: ----->类和方法__25+[Manager noticeRequest:]_block_invoke399----->错误信息Error Domain= ...