❖ 理解Spring IoC的原理
❖ 掌握Spring IoC的配置
❖ 理解Spring AOP的原理
❖ 掌握Spring AOP的配置
任务1:认识Spring
任务2:Spring IoC的简单运用
任务3:Spring AOP的简单运用
➢ 了解Spring的优点。
➢ 了解Spring的整体架构。
企业级应用是指为商业组织、大型企业创建并部署的解决方案及应用。这些大型企业级应用的结构复杂,涉及的外部资源众多,事务密集、数据规模大、用户数量多,有较强的安全性考虑和较高的性能要求。
企业级应用绝不可能是一个个的独立系统。在企业中,一般都会部署多个交互的应用,同时这些应用又有可能与其他企业的相关应用连接,从而构成一个结构复杂的、跨越Internet的分布式企业应用集群。此外,作为企业级应用,不但要有强大的功能,还要能够满足未来业务需求的发展变化,易于扩展和维护。
传统Java EE在解决企业级应用问题时的“重量级”架构体系,使它的开发效率、开发难度和实际性能都令人失望。当人们苦苦寻找解决办法的时候,Spring以一个“救世主”的形象出现在广大Java程序员面前。说到Spring,就要提到Rod Johnson,2002年他编写了《Expert One-on-One Java EE Design and Development》一书。在书中,他对传统Java EE技术的日益臃肿和低效提出了质疑,他觉得应该有更便捷的做法,于是提出了Interface 21,也就是Spring框架的雏形。他提出了技术应以实用为准的主张,引发了人们对“正统”Java EE的反思。2003年2月,Spring框架正式成为一个开源项目,并发布于SourceForge中。
Spring致力于Java EE应用的各种解决方案,而不仅仅专注于某一层的方案。可以说,Spring是企业应用开发的“一站式”选择,贯穿表现层、业务层和持久层。并且Spring并不想取代那些已有的框架,而是以高度的开放性与它们无缝整合。
Spring确实给人一种格外清新的感觉,仿佛微雨后的绿草丛,蕴藏着勃勃生机。Spring是一个轻量级框架,它大大简化了Java企业级开发,提供强大、稳定功能的同时并没有带来额外的负担。Spring有两个主要目标:一是让现有技术更易于使用,二是养成良好的编程习惯(或者称为最佳实践)。
作为一个全面的解决方案,Spring坚持一个原则:不重新发明轮子。已经有较好解决方案的领域,Spring绝不做重复性的实现。例如,对象持久化和ORM, Spring只是对现有的JDBC、MyBatis、Hibernate等技术提供支持,使之更易用,而不是重新实现。
Spring框架由大约20个功能模块组成。这些模块被分成六个部分,分别是Core Container、Data Access/Integration、Web、AOP(Aspect Oriented Programming)、Instrumentation及Test
Spring Core是框架的最基础部分,提供了IoC特性。SpringContext为企业级开发提供了便利的集成工具。Spring AOP是基于Spring Core的符合规范的面向切面编程的实现。Spring JDBC提供了JDBC的抽象层,简化了JDBC编码,同时使代码更健壮。SpringORM对市面上流行的ORM框架提供了支持。Spring Web为Spring在Web应用程序中的使用提供了支持。关于Spring的其他功能模块在开发中的作用,可以查阅Spring的文档进行了解
关键步骤如下。
➢ 掌握IoC的原理。
➢ 使用IoC的设值注入方式输出“Hello, Spring! ”。
➢ 使用IoC的设值注入方式实现动态组装的打印机。
控制反转(Inversion of Control, IoC)也称为依赖注入(Dependency Injection,DI),是面向对象编程中的一种设计理念,用来降低程序代码之间的耦合度。
依赖一般指通过局部变量、方法参数、返回值等建立的对于其他对象的调用关系。例如,在A类的方法中,实例化了B类的对象并调用其方法来完成特定的功能,我们就说A类依赖于B类。
几乎所有的应用都由两个或更多的类通过合作来实现完整的功能。类与类之间的依赖关系增加了程序开发的复杂程度,我们在开发一个类的时候,还要考虑对正在使用该类的其他类的影响。例如,常见的业务层调用数据访问层以实现持久化操作
/**
*用户Dao接口,定义了所需的持久化方法
**/
public interface UserDao{
public void save(User user);
}
/**
*用户dao实现类,实现对user类的持久化操作
**/
public clsaa UserDaoImpl implements UserDao{
public void save(User user){
System.out.print("保存数据库信息到数据库");
}
}
/**
*用户业务类,实现对user功能的业务管理
**/
public class UserServiceImpl implements UserServicxe{
private UserDao dao=new UserDaoImpl();
public void addNewUser(User user){
dao.save(user);
}
}
工厂模式:根据我们提供的所需要的对象实例的描述,为我们返回所需的产品
1.产品的规范
2.产品
3.工厂
4.客户端/调用
如以上代码所示,UserServiceImpl对UserDaoImpl存在依赖关系。这样的代码很常见,但是存在一个严重的问题,即UserServiceImpl和UserDaoImpl高度耦合,如果因为需求变化需要替换UserDao的实现类,将导致UserServiceImpl中的代码随之发生修改。如此,程序将不具备优良的可扩展性和可维护性,甚至在开发中难以测试。
我们可以利用简单工厂和工厂方法模式的思路解决此类问题
public class UserDaoFactory{
public static UserDao getInstance(){
}
}
public class UserServiceImpl implements UserService{
private UserDao dao=UserDaoFactory.getInstance();
public void addNewUser(User user){
dao.save(user);
}
}
示例2中的用户DAO工厂类UserDaoFactory体现了“控制反转”的思想:User ServiceImpl不再依靠自身的代码去获得所依赖的具体DAO对象,而是把这一工作转交给了“第三方”UserDaoFactory,从而避免了和具体UserDao实现类之间的耦合。由此可见,在如何获取所依赖的对象上,“控制权”发生了“反转”,即从UserServiceImpl转移到了UserDaoFactory,这就是“控制反转”。
问题虽然得到了解决,但是大量的工厂类被引入开发过程中,明显增加了开发的工作量。而Spring能够分担这些额外的工作,其提供了完整的IoC实现,让我们得以专注于业务类和DAO类的设计。
问题我们已经了解了“控制反转”,
那么在项目中如何使用Spring实现“控制反转”呢?开发第一个Spring项目,输出“Hello, Spring! ”。
具体要求如下。
➢ 编写HelloSpring类输出“Hello, Spring ! ”。
➢ 其中的字符串内容“Spring”是通过Spring框架赋值到HelloSpring类中的。
实现思路及关键代码
(1)下载Spring并添加到项目中。
(2)编写Spring配置文件。
(3)编写代码通过Spring获取HelloSpring实例。
首先通过Spring官网http://repo.spring.io/release/org/springframework/spring/下载所需版本的Spring资源,这里以Spring Framework 3.2.13版本为例。下载的压缩包spring-framework-3.2.13.RELEASE-dist.zip解压后的文件夹目录结构如图4.3所示
➢ docs:该文件夹下包含Spring的相关文档,包括API参考文档、开发手册。
➢ libs:该文件夹下存放Spring各个模块的jar文件,每个模块均提供三项内容:开发所需的jar文件、以“-javadoc”后缀表示的API和以“-sources”后缀表示的源文件。
➢ schema:配置Spring的某些功能时需要用到的schema文件,对于已经集成了Spring的IDE环境(如MyEclipse),这些文件不需要专门导入。
在MyEclipse中新建一个项目HelloSpring,将所需的Spring的jar文件添加到该项目中。需要注意的是,Spring的运行依赖于commons-logging组件,需要将相关jar文件一并导入。为了方便观察Bean实例化过程,我们采用log4j作为日志输出,所以也应该将log4j的jar文件添加到项目中。
为项目添加log4j.properties文件,用来控制日志输出。log4j.properties文件
# rootLogger是所有日志的根日志,修改该日志属性将对所有日志起作用
# 下面的属性配置中,所有日志的输出级别是info,输出源是con
log4j.rootLogger=info,con
# 定义输出源的输出位置是控制台
log4j.appender.con=org.apache.log4j.ConsoleAppender
# 定义输出日志的布局采用的类
log4j.appender.con.layout=org.apache.log4j.PatternLayout
# 定义日志输出布局
log4j.appender.con.layout.ConversionPattern=%d{
MM-dd HH:mm:ss}[%p]%c%n -%m%n
编写HelloSpring类
public class HelloSpring {
// 定义who属性,该属性的值将通过Spring框架进行设置
private String who = null;
/**
* 定义打印方法,输出一句完整的问候。
*/
public void print() {
System.out.println("Hello," + this.getWho() + "!");
}
/**
* 获得 who。
*
* @return who
*/
public String getWho() {
return who;
}
/**
* 设置 who。
*
* @param who
*/
public void setWho(String who) {
this.who = who;
}
}
接下来编写Spring配置文件,在项目的classpath根路径下创建applicationContext. xml文件(为便于管理框架的配置文件,可在项目中创建专门的源文件夹,如resources目录,并将Spring配置文件创建在其根路径下)。在Spring配置文件中创建HelloSpring类的实例并为who属性注入属性值。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
<!-- 通过bean元素声明需要Spring创建的实例。该实例的类型通过class属性指定,并通过id属性为该实例指定一个名称,以便在程序中使用 -->
<bean id="helloSpring" class="cn.springdemo.HelloSpring">
<!-- property元素用来为实例的属性赋值,此处实际是调用setWho()方法实现赋值操作 -->
<property name="who">
<!-- 此处将字符串"Spring"赋值给who属性 -->
<value>Spring</value>
</property>
</bean>
</beans>
在Spring配置文件中,使用元素来定义Bean(也可称为组件)的实例。元素有两个常用属性:一个是id,表示定义的Bean实例的名称;另一个是class,表示定义的Bean实例的类型。
经验
(1)使用元素定义一个组件时,通常需要使用id属性为其指定一个用来访问的唯一名称。如果想为Bean指定更多的别名,可以通过name属性指定,名称之间使用逗号、分号或空格进行分隔。(2)在本例中,Spring为Bean的属性赋值是通过调用属性的setter方法实现的,这种做法称为“设值注入”,而非直接为属性赋值。若属性名为who, setter方法名为setSomebody( ), Spring配置文件中应写成name="somebody"而非name=“who”。所以在为属性和setter访问器命名时,一定要遵循JavaBean的命名规范。
在项目中添加测试方法
@Test
public void helloSpring() {
// 通过ClassPathXmlApplicationContext实例化Spring的上下文
ApplicationContext context = new ClassPathXmlApplicationContext(
"applicationContext.xml");
// 通过ApplicationContext的getBean()方法,根据id来获取bean的实例
HelloSpring helloSpring = (HelloSpring) context.getBean("helloSpring");
// 执行print()方法
helloSpring.print();
}
ApplicationContext是一个接口,负责读取Spring配置文件,管理对象的加载、生成,维护Bean对象之间的依赖关系,负责Bean的生命周期等。ClassPat hXmlApplicationContext是ApplicationContext接口的实现类,用于从classpath路径中读取Spring配置文件。
知识扩展
(1)除了ClassPathXmlApplicationContext,ApplicationContext接口还有其他实现类。例如,FileSystemXmlApplicationContext也可以用于加载Spring配置文件。(2)除了ApplicationContext及其实现类,还可以通过BeanFactory接口及其实现类对Bean组件实施管理。事实上,ApplicationContext就是建立在BeanFactory的基础之上。BeanFactory接口是Spring IoC容器的核心,负责管理组件和它们之间的依赖关系,应用程序通过BeanFactory接口与Spring IoC容器交互。ApplicationContext是BeanFactory的子接口,可以对企业级开发提供更全面的支持。
通过“Hello, Spring! ”的例子,我们发现Spring会自动接管配置文件中Bean的创建和为属性赋值的工作。Spring在创建Bean的实例后,会调用相应的setter方法为实例设置属性值。实例的属性值将不再由程序中的代码来主动创建和管理,改为被动接受Spring的注入,使得组件之间以配置文件而不是硬编码的方式组织在一起。
提示
相对于“控制反转”, “依赖注入”的说法也许更容易理解一些,即由容器(如Spring)负责把组件所“依赖”的具体对象“注入”(赋值)给组件,从而避免组件之间以硬编码的方式耦合在一起。
如何开发一个打印机模拟程序,使其符合以下条件。
➢ 可以灵活地配置使用彩色墨盒或灰色墨盒。
➢ 可以灵活地配置打印页面的大小。
分析程序中包括打印机(Printer)、墨盒(Ink)和纸张(Paper)三类组件,如图4.5所示。打印机依赖墨盒和纸张。采取如下的步骤开发这个程序。
(1)定义Ink和Paper接口。
(2)使用Ink接口和Paper接口开发Printer程序。在开发Printer程序时并不依赖Ink和Paper的具体实现类。
(3)开发Ink接口和Paper接口的实现类:ColorInk、GreyInk和TextPaper。
(4)组装打印机,运行调试。
1.定义Ink和Paper接口
public interface Ink {
/**
* 定义打印采用的颜色的方法。
*
* @param r
* 红色值
* @param g
* 绿色值
* @param b
* 蓝色值
* @return 返回打印采用的颜色
*/
public String getColor(int r, int g, int b);
}
public interface Paper {
public static final String newline = "\r\n";
/**
* 输出一个字符到纸张。
*/
public void putInChar(char c);
/**
* 得到输出到纸张上的内容。
*/
public String getContent();
}
2.使用Ink接口和Paper接口开发Printer程序
/**
* 打印机程序。
*
* @author
*/
public class Printer {
// 面向接口编程,而不是具体的实现类
private Ink ink = null;
// 面向接口编程,而不是具体的实现类
private Paper paper = null;
/**
* 设值注入所需的setter方法。
*
* @param ink
* 传入墨盒参数
*/
public void setInk(Ink ink) {
this.ink = ink;
}
/**
* 设值注入所需的setter方法。
*
* @param paper
* 传入纸张参数
*/
public void setPaper(Paper paper) {
this.paper = paper;
}
/**
* 打印机打印方法。
*
* @param str
* 传入打印内容
*/
public void print(String str) {
// 输出颜色标记
System.out.println("使用" + ink.getColor(255, 200, 0) + "颜色打印:\n");
// 逐字符输出到纸张
for (int i = 0; i < str.length(); ++i) {
paper.putInChar(str.charAt(i));
}
// 将纸张的内容输出
System.out.print(paper.getContent());
}
}
Printer类中只有一个print()方法,输入参数是一个即将被打印的字符串,打印机将这个字符串逐个字符输出到纸张,然后将纸张中的内容输出。
在开发Printer程序的时候,只需要了解Ink接口和Paper接口即可,完全不需要依赖这些接口的某个具体实现类,这是符合实际情况的。在设计真实的打印机时也是这样,设计师只是针对纸张和墨盒的接口规范进行设计。在使用时,只要符合相应的规范,打印机就可以根据需要更换不同的墨盒和纸张。
软件设计与此类似,由于明确地定义了接口,在编写代码的时候,完全不用考虑和某个具体实现类的依赖关系,从而可以构建更复杂的系统。组件间的依赖关系和接口的重要性在将各个组件组装在一起的时候得以体现。通过这种开发模式,还可以根据需要方便地更换接口的实现,就像为打印机更换不同的墨盒和纸张一样。Spring提倡面向接口编程也是基于这样的考虑。
print()方法运行的时候是从哪里获得Ink和Paper的实例呢?
这时就需要提供“插槽”,以便组装的时候可以将Ink和Paper的实例“注入”进来,对Java代码来说就是定义setter方法。至此,Printer类的开发工作就完成了。
3.开发Ink接口和Paper接口的实现类:ColorInk、GreyInk和TextPaper
/**
* 彩色墨盒。ColorInk实现Ink接口。
*
* @author
*/
public class ColorInk implements Ink {
// 打印采用彩色
public String getColor(int r, int g, int b) {
Color color = new Color(r, g, b);
return "#" + Integer.toHexString(color.getRGB()).substring(2);
}
}
/**
* 灰色墨盒。GreyInk实现Ink接口。
*
* @author
*/
public class GreyInk implements Ink {
// 打印采用灰色
public String getColor(int r, int g, int b) {
int c = (r + g + b) / 3;
Color color = new Color(c, c, c);
return "#" + Integer.toHexString(color.getRGB()).substring(2);
}
}
彩色墨盒的getColor()方法对传入的颜色参数做了简单的格式转换;灰色墨盒则对传入的颜色值进行计算,先转换成灰度颜色,再进行格式转换。这不是需要关注的重点,了解其功能即可。
/**
* 文本打印纸张实现。TextPaper实现Paper接口。
*
* @author
*/
public class TextPaper implements Paper {
// 每行字符数
private int charPerLine = 16;
// 每页行数
private int linePerPage = 5;
// 纸张中内容
private String content = "";
// 当前横向位置,从0到charPerLine-1
private int posX = 0;
// 当前行数,从0到linePerPage-1
private int posY = 0;
// 当前页数
private int posP = 1;
public String getContent() {
String ret = this.content;
// 补齐本页空行,并显示页码
if (!(posX == 0 && posY == 0)) {
int count = linePerPage - posY;
for (int i = 0; i < count; ++i) {
ret += Paper.newline;
}
ret += "== 第" + posP + "页 ==";
}
return ret;
}
public void putInChar(char c) {
content += c;
++posX;
// 判断是否换行
if (posX == charPerLine) {
content += Paper.newline;
posX = 0;
++posY;
}
// 判断是否翻页
if (posY == linePerPage) {
content += "== 第" + posP + "页 ==";
content += Paper.newline + Paper.newline;
posY = 0;
++posP;
}
}
// setter方法,用于属性注入
public void setCharPerLine(int charPerLine) {
this.charPerLine = charPerLine;
}
// setter方法,用于属性注入
public void setLinePerPage(int linePerPage) {
this.linePerPage = linePerPage;
}
}
在TextPaper实现类的代码中,我们不用关心具体的逻辑实现,只需理解其功能即可。其中content用于保存当前纸张的内容。charPerLine和linePerPage用于限定每行可以打印多少个字符和每页可以打印多少行。需要注意的是,setCharPerLine()和setLinePerPage()这两个setter方法,与示例9中的setter方法类似,也是为了组装时“注入”数据留下的“插槽”。我们不仅可以注入某个类的实例,还可以注入基本数据类型、字符串等类型的数据。
4.组装打印机,运行调试
组装打印机的工作在Spring的配置文件(applicationContext.xml)中完成。首先,创建几个待组装零件的实例
各“零件”都定义好后,下面来完成打印机的组装
组装了一台彩色的、使用B5打印纸的打印机。需要注意的是,这里没有使用的value属性,而是使用了ref属性。value属性用于注入基本数据类型以及字符串类型的值。ref属性用于注入已经定义好的Bean,如刚刚定义好的colorInk、greyInk、a4Paper和b5Paper。由于Printer的setInk(Ink ink)方法要求传入的参数是Ink(接口)类型,所以任何Ink接口的实现类都可以注入。
完整的Spring配置文件,applicationContext.xml
从配置文件中可以看到Spring管理Bean的灵活性。Bean与Bean之间的依赖关系放在配置文件里组织,而不是写在代码里。通过对配置文件的指定,Spring能够精确地为每个Bean注入属性。
每个Bean的id属性是该Bean的唯一标识。程序通过id属性访问Bean,Bean与Bean的依赖关系也通过id属性完成。
打印机组装好之后如何工作呢?测试方法的关键代码
*/
public class PrinterTest {
@Test
public void printerTest() {
ApplicationContext context = new ClassPathXmlApplicationContext(
"applicationContext.xml");
// 通过Printer bean的id来获取Printer实例
Printer printer = (Printer) context.getBean("printer");
String content = "几位轻量级容器的作者曾骄傲地对我说:这些容器非常有"
+ "用,因为它们实现了“控制反转”。这样的说辞让我深感迷惑:控"
+ "制反转是框架所共有的特征,如果仅仅因为使用了控制反转就认为"
+ "这些轻量级容器与众不同,就好像在说“我的轿车是与众不同的," + "因为它有4个轮子。”";
printer.print(content);
}
}
至此,打印机已经全部组装完成并可以正常使用了。现在我们来总结一下:和Spring有关的只有组装和运行两部分代码。仅这两部分代码就让我们获得了像更换打印机的墨盒和打印纸一样更换程序组件的能力。这就是Spring依赖注入的魔力。
通过Spring的强大组装能力,我们在开发每个程序组件的时候,只要明确关联组件的接口定义,而不需要关心具体实现,这就是所谓的“面向接口编程”。
关键步骤如下。
➢ 掌握Spring AOP的原理。
➢ 使用Spring AOP实现自动的系统日志功能。
面向切面编程(Aspect Oriented Programming, AOP)是软件编程思想发展到一定阶段的产物,是对面向对象编程(ObjectOriented Programming, OOP)的有益补充。AOP一般适用于具有横切逻辑的场合,如访问控制、事务管理、性能监测等。
什么是横切逻辑呢?
UserService的addNewUser()方法根据需求增加了日志和事务功能。
这是一个再典型不过的业务处理方法。日志、异常处理、事务控制等,都是一个健壮的业务系统所必需的。但为了保证系统健壮可用,就要在众多的业务方法中反复编写类似的代码,使得原本就很复杂的业务处理代码变得更加复杂。业务功能的开发者还要关注这些“额外”的代码是否处理正确,是否有遗漏。如果需要修改日志信息的格式或者安全验证的规则,或者再增加新的辅助功能,都会导致业务代码频繁而大量的修改
在业务系统中,总有一些散落、渗透到系统各处且不得不处理的事情,这些穿插在既定业务中的操作就是所谓的“横切逻辑”,也称为切面。怎样才能不受这些附加要求的干扰,专注于真正的业务逻辑呢?我们很容易想到的就是将这些重复性的代码抽取出来,放在专门的类和方法中处理,这样就便于管理和维护了。但即便如此,依然无法实现既定业务和横切逻辑的彻底解耦合,因为业务代码中还要保留对这些方法的调用代码,当需要增加或减少横切逻辑的时候,还是要修改业务方法中的调用代码才能实现。我们希望的是无须编写显式的调用,在需要的时候,系统能够“自动”调用所需的功能,这正是AOP要解决的主要问题。
面向切面编程,简单地说就是在不改变原有程序的基础上为代码段增加新的功能,对其进行增强处理。它的设计思想来源于代理设计模式,下面以图示的方式进行简单的说明。通常情况下调用对象的方法如图所示
在代理模式中可以为对象设置一个代理对象,代理对象为fun()提供一个代理方法,当通过代理对象的fun()方法调用原对象的fun()方法时,就可以在代理方法中添加新的功能,这就是所谓的增强处理。增强的功能既可以插到原对象的fun()方法前面,也可以插到其后面,如图4.8所示。
在这种模式下,给编程人员的感觉就是在原有代码乃至原业务流程都不改变的情况下,直接在业务流程中切入新代码,增加新功能,这就是所谓的面向切面编程。对面向切面编程有了感性认识以后,还需要了解它的一些基本概念
。➢ 切面(Aspect):一个模块化的横切逻辑(或称横切关注点),可能会横切多个对象。
➢ 连接点(Join Point):程序执行中的某个具体的执行点。图4.8中原对象的fun()方法就是一个连接点。
➢ 增强处理(Advice):切面在某个特定连接点上执行的代码逻辑。
➢ 切入点(Pointcut):对连接点的特征进行描述,可以使用正则表达式。增强处理和一个切入点表达式相关联,并在与这个切入点匹配的某个连接点上运行。
➢ 目标对象(Target object):被一个或多个切面增强的对象。
➢ AOP代理(AOP proxy):由AOP框架所创建的对象,实现执行增强处理方法等功能。
➢ 织入(Weaving):将增强处理连接到应用程序中的类型或对象上的过程。
➢ 增强处理类型:如图4.8所示,在原对象的fun()方法之前插入的增强处理为前置增强,在该方法正常执行完以后插入的增强处理为后置增强,此外还有环绕增强、异常抛出增强、最终增强等类型。
说明
切面可以理解为由增强处理和切入点组成,既包含了横切逻辑的定义,也包含了连接点的定义。面向切面编程主要关心两个问题,即在什么位置执行什么功能。Spring AOP是负责实施切面的框架,即由Spring AOP完成织入工作。Advice直译为“通知”,但这种叫法并不确切,在此处翻译成“增强处理”,更便于理解。
问题
日志输出的代码直接嵌入在业务流程的代码中,不利于系统的扩展和维护。如何使用Spring AOP来实现日志输出,以解决这个问题呢?
实现思路及关键代码
(1)在项目中添加Spring AOP相关的jar文件。
(2)编写前置增强和后置增强实现日志功能。
(3)编写Spring配置文件,对业务方法进行增强处理。
(4)编写代码,获取带有增强处理的业务对象
首先在项目中添加所需的jar文件,jar文件清单如图4.10所示。spring-aop-3.2.13. RELEASE.jar提供了Spring AOP的实现。同时,Spring AOP还依赖AOP Alliance和AspectJ项目中的组件,相关版本的下载链接分别为https://sourceforge.net/projects/aopalliance/files/aopalliance/1.0/和http://mvnrepository.com/artifact/org.aspectj/aspectjweaver。
接下来,编写业务类UserServiceImpl
package service.impl;
import service.UserService;
import dao.UserDao;
import entity.User;
/**
* 用户业务类,实现对User功能的业务管理
*/
public class UserServiceImpl implements UserService {
// 声明接口类型的引用,和具体实现类解耦合
private UserDao dao;
// dao 属性的setter访问器,会被Spring调用,实现设值注入
public void setDao(UserDao dao) {
this.dao = dao;
}
public void addNewUser(User user) {
// 调用用户DAO的方法保存用户信息
dao.save(user);
}
}
UserServiceImpl业务类中有一个addNewUser()方法,实现用户业务的添加。可以发现,在该方法中并没有实现日志输出功能,接下来就以AOP的方式为该方法添加日志功能。编写增强类
package aop;
import java.util.Arrays;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
public class UserServiceLogger {
private static final Logger log = Logger.getLogger(UserServiceLogger.class);
public void before(JoinPoint jp) {
log.info("调用 " + jp.getTarget() + " 的 " + jp.getSignature().getName()
+ " 方法。方法入参:" + Arrays.toString(jp.getArgs()));
}
public void afterReturning(JoinPoint jp, Object result) {
log.info("调用 " + jp.getTarget() + " 的 " + jp.getSignature().getName()
+ " 方法。方法返回值:" + result);
}
}
UserServiceLogger类中定义了before()和afterReturning()两个方法。我们希望把before()方法作为前置增强使用,即将该方法添加到目标方法之前执行;把afterReturning()方法作为后置增强使用,即将该方法添加到目标方法正常返回之后执行。这里先以前置增强和后置增强为例
为了能够在增强方法中获得当前连接点的信息,以便实施相关的判断和处理,可以在增强方法中声明一个JoinPoint类型的参数,Spring会自动注入实例。通过实例的getTarget()方法得到被代理的目标对象,通过getSignature()方法返回被代理的目标方法,通过getArgs()方法返回传递给目标方法的参数数组。对于实现后置增强的afterReturning()方法,还可以定义一个参数用于接收目标方法的返回值。
在Spring配置文件中对相关组件进行声明。
接下来在Spring配置文件中进行AOP相关的配置,首先定义切入点
注意
在元素中需要添加aop的名称空间,以导入与AOP相关的标签。
与AOP相关的配置都放在aop:config标签中,如配置切入点的标签aop:pointcut。aop:pointcut的expression属性可以配置切入点表达式,
execution(public void addNewUser(entity.User))
execution是切入点指示符,括号中是一个切入点表达式,用于配置需要切入增强处理的方法的特征。切入点表达式支持模糊匹配,下面介绍几种常用的模糊匹配。
➢ public * addNewUser(entity.User): “*”表示匹配所有类型的返回值。
➢ public void (entity.User): “”表示匹配所有方法名。
➢ public void addNewUser(…): “…”表示匹配所有参数个数和类型。
➢ * com.service..(…):这个表达式匹配com.service包下所有类的所有方法。
➢ * com.service….(…):这个表达式匹配com.service包及其子包下所有类的所有方法。
具体使用时可以根据自己的需求来设置切入点的匹配规则。当然,匹配的规则和关键字还有很多,可以参考Spring的开发手册学习。
最后还需要在切入点处插入增强处理,这个过程的专业叫法是“织入”。实现织入的配置代码
在aop:config中使用aop:aspect引用包含增强方法的Bean,然后分别通过aop:before和aop:after-returning将方法声明为前置增强和后置增强,在aop:after-returning中通过returning属性指定需要注入返回值的属性名。方法的JoinPoint类型参数无须特殊处理,Spring会自动为其注入连接点实例。很明显,UserService的addNewUser()方法可以和切入点pointcut相匹配,Spring会生成代理对象,在它执行前后分别调用before()和afterReturning()方法,这样就完成了日志输出。
编写测试代码
package test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import service.UserService;
import service.impl.UserServiceImpl;
import entity.User;
public class AopTest {
@Test
public void aopTest() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService service = (UserService) ctx.getBean("service");
User user = new User();
user.setId(1);
user.setUsername("test");
user.setPassword("123456");
user.setEmail("[email protected]");
service.addNewUser(user);
}
}
从以上示例可以看出,业务代码和日志代码是完全分离的,经过AOP的配置以后,不做任何代码上的修改就在addNewUser()方法前后实现了日志输出。其实,只需稍稍修改切入点的指示符,不仅可以为UserService的addNewUser()方法增强日志功能,也可以为所有业务方法进行增强;并且可以增强日志功能,如实现访问控制、事务管理、性能监测等实用功能。
本章总结
➢ Spring是一个轻量级的企业级框架,提供了IoC容器、AOP实现、DAO/ORM支持、Web集成等功能,目标是使现有的Java EE技术更易用,并形成良好的编程习惯。
➢ 依赖注入让组件之间以配置文件的形式组织在一起,而不是以硬编码的方式耦合在一起。
➢ Spring配置文件是完成组装的主要场所,常用节点包括及其子节点。
➢ AOP的目的是从系统中分离出切面,将其独立于业务逻辑实现,并在程序执行时织入程序中运行。
➢ 面向切面编程主要关心两个问题:在什么位置,执行什么功能。
➢ 配置AOP主要使用aop命名空间下的元素完成,可以完成定义切入点和织入增强等操作。