接着上一篇文章的内容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. React躬行记(9)——组件通信

    根据组件之间的嵌套关系(即层级关系)可分为4种通信方式:父子.兄弟.跨级和无级. 一.父子通信 在React中,数据是自顶向下单向流动的,而父组件通过props向子组件传递需要的信息是组件之间最常见的 ...

  2. SpringBoot2.0集成WebSocket,实现后台向前端推送信息

    感谢作者,支持原创: https://blog.csdn.net/moshowgame/article/details/80275084 什么是WebSocket? WebSocket协议是基于TCP ...

  3. 从0系统学Android-2.4隐式Intent

    本系列文章,参考<第一行代码>,作为个人笔记 更多内容:更多精品文章分类 使用隐式 Intent 相对于显示 Intent ,隐式 Intent 比较含蓄.这种方式不明确指出我们想要启动哪 ...

  4. 从微信小程序开发者工具源码看实现原理(四)- - 自适应布局

    从前面从微信小程序开发者工具源码看实现原理(一)- - 小程序架构设计可以知道,小程序大部分是通过web技术进行渲染的,也就是最终通过浏览器的dom tree + cssom来生成渲染树:既然最终是通 ...

  5. PHP对接口执行效率慢的优化

    PHP对接口执行效率慢的优化 PHP对接口执行效率慢的优化 造成执行效率低的原因可以由很多方面找原因 从代码层面,代码质量低,执行效率也会有很大影响的. 从硬件方面,服务器配置低,服务器配置是基础,这 ...

  6. SQL SERVER中生僻字问题存储与查询问题

    以下仅记录碰到的几个问题 1.首先字段设置为varchar的时候存储后无法进行正常的显示 显示为? 此状态下匹配查询或者Like模糊查询都没问题 2.将字段设置为nvarchar,在进行插入或者跟新时 ...

  7. java练习---1

    //程序员:罗元昊 2017.9.6public class Ap{ public static void main(String[] args){ System.out.println(" ...

  8. 用wxpy管理微信公众号,并利用微信获取自己的开源数据。

    之前了解到itchat 乃至于 wxpy时 是利用tuling聊天机器人的接口.调用接口并保存双方的问答结果可以作为自己的问答词库的一个数据库累计.这些数据可以用于自己训练. 而最近希望获取一些语音资 ...

  9. IDEA自定义配置

    目录 1 常规设置 1 修改字体大小 2 创建文件时 增加注释信息 3 项目编码为UTF-8 4 properties 文件编码为UTF-8且Transparent native-to-ascii c ...

  10. Java编程基础阶段笔记 day06 二维数组

    二维数组 笔记Notes 二维数组 二维数组声明 二维数组静态初始化与二位初始化 二维数组元素赋值与获取 二维数组遍历 二维数组内存解析 打印杨辉三角 Arrays工具类 数组中常见的异常 二维数组 ...