1、13.1 SpringFramework 概述13.1.1 什么是 SpringFrameworkSpring 是一个轻量级的容器框架,具有 IoC 和 AOP 特性。Spring 是一个应用框架(相对于 Java EE 标准) ,Struts 、JSF 和 Tapestry 是Web 层框架,Hibernate 是持久层框架,而 Spring 关注应用的所有层。Spring 在每一层都提供了相应的支持,例如在 Web 层提供了 Spring MVC 框架,在 dao层提供了 Spring JdbcTemplate,另外 Spring 也可以通过引入其它的框架把其他框架嵌入到 Spring 中
2、,例如可以通过 ORM 层嵌入 Hibernate、Toplink 等 ORM框架,在 Web 层可以嵌入 Struts 和 JSF 等 Web 层框架。Spring 作为容器,对运行在其中的组件进行管理,包括组件的创建、删除以及组件之间关系的设置。根据需要可以只生成一个实例,也可以每次请求生成单独的实例。Spring 是轻量级的,从大小和开销两方面来说,Spring 都是轻量级的,完整的 Spring 框架可以在大小只有 1M 多的 jar 文件中发布。并且 Spring 所需要的处理开销也是微不足道的。此外,Spring 是非侵入式的,在开发过程中不依赖于特定的 Spring 类。Spri
3、ng 采用了控制反转(Inverse of Controll,IoC) ,或者称为依赖注入(Dependency Injection,DI)的技术。采用 IoC 之后,一个对象依赖的其他对象不需要自己创建,可以由 IoC 容器创建,然后容器把这个对象赋给它。Spring 提供了对面向切面编程(Aspect Oriented Programming,AoP)的支持,允许通过分离应用的业务逻辑和系统级服务进行内聚性的开发。应用程序只实现他们应该做的,完成核心业务逻辑,他们并不负责其他的系统级服务关注点,例如日志、安全和事务处理。下面分别介绍 Spring 的两个关键特性 IoC 和 AOP。Spr
4、ing 框架提供的功能一个 IoC 容器,用于关于业务类;一个 AoP 框架,Spring 提供了一个基于代理的 AOP 框架,并且可以和其他的 AOP 框架集成,例如 AspectJ or AspectWerkz;一个服务抽象层,能够采用相同的方式与标准的 API 以及第三方的 API 集成。服务抽象层:提供了对如下服务的抽象提取:事务管理:JTA、数据库事务以及其他的事务数据访问:JDBC、Hibernate、JDO、iBatis、TopLinkEmail路由(Remoting):EJB, Web Services, RMI, Hessian/BurlapSpring 的 Web 层可以很
5、好的与 Struts、WebWork、JSF、Tapestry、Velocity 以及其他 Web 框架进行集成,除了可以与这些框架进行集成之外,还提供了自己的 Web 层框架 Spring Web MVC。Spring 与 JavaEE 应用服务器Spring 不是 JavaEE 应用服务器。它可以很好的与 Java 应用服务器进行集成,很多情况下,他和可以替换原本需要应用服务器提供的功能。使用方便,你的代码不能依赖 Spring,充分利用和集成现有优秀的框架。Spring 是非侵入的,用户在编写代码的时候不用引入或者继承任何 Spring 的 API。不像在 Struts1 中所有的 Ac
6、tion 需要继承 Action,FormBean 需要继承 ActionForm。EJB2 中的 Bean 类需要实现接口, home 接口和业务接口都需要继承特定的接口。13.1.2 AOPAOP(Aspect-Oriented Programming) ,面向切面的编程,或者面向方面的编程,与OOP 相对, OOP 关注的对象,关注的是继承关系,是一种层次关系,而 AOP 关注的是应用的横切面,与应用的核心业务无关,但是在执行这些核心业务的时候都会用到。例如日志功能。AOP 关注的不再是对象,而是应用的某些关注点,例如日志、事务、安全,Spring 提供了这些服务。AOP 的基本概念:
7、方面(Aspect):切入多个类的关注的模块化表示,通常是一个类,封装了在发生所关注的事件之后需要执行的操作。日志和事务管理就是很好的例子,在很多类中都需要进行日志和事务处理,日志和事务处理就可以使用单独的类来实现,就是方面。在 Spring 中可以使用普通类或者使用Aspect 注释进行标注的普通类。Struts2 中编写拦截器来实现AOP 的方面。 连接点(Joinpoint):程序执行过程中的一个点,可能是方法的执行,可能是异常的处理,例如添加用户、删除用户等。在 Spring AOP 中,连接点通常表示一个方法的执行。 通知(Advice):在特定的连接点方面要执行的动作。通知包括环绕
8、通知(around) 、事前通知( before)和事后通知(after )以及异常通知(throws) 。许多 AOP 框架包括 Spring 都是把通知设计成拦截器,为每个连接点维护一个拦截器链。例如,可以为添加用户的方法定义两个通知,分别为事务处理和日志处理。各种通知区别如下: Around 通知:包围一个连接点的通知,如方法调用。这是最强大的通知。Aroud 通知在方法调用前后完成自定义的行为,它们负责选择继续执行连接点或通过返回它们自己的返回值或抛出异常来短路执行。 Before 通知:在一个连接点之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常) 。 Throw
9、s 通知:在方法抛出异常时执行的通知。Spring 提供强制类型的 Throws 通知,因此你可以书写代码捕获感兴趣的异常(和它的子类) ,不需要从 Throwable 或 Exception 强制类型转换。 After returning 通知:在连接点正常完成后执行的通知,例如,一个方法正常返回,没有抛出异常。 Pointcut:一个通知与连接点进行匹配的断言。通知通常与一个 pointcut关联,当关联点与 pointcut 匹配的时候,会执行关联的通知。在 Spring中,默认使用 AspectJ pointcut 表达式语言编写 pointcut。 引入(Introduction):
10、添加方法或字段到被通知的类。Spring 允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified 接口,来简化缓存。 目标对象(Target Object):被一个或多个方面通知的对象,也可以称作被通知对象。因为 Spring Aop 使用运行时代理,这个对象通常是一个被代理对象。 AOP 代理(AOP Proxy):AOP 框架为了实现面向方面的编程而创建的对象,例如通知方法执行等。在 Spring 中,AOP 代理是 JDK 动态代理或 CGLIB 代理。 编织(Weaving):组装方面来创建一个被通知对象。这可以在编译时完成(例如使用 Aspe
11、ctJ 编译器) ,也可以在运行时完成。Spring 和其他纯 Java AOP 框架一样,在运行时完成织入。只要在调用目标对象的某一个业务方法时候,能够拦截该方法的调用,就可以将切面织入到应用程序的流程中。AOP 的实现,可以有 3 种方式:(1)编译期:编译的时候,把切面加入到业务中,需要有特殊的编译器把两个文件的内容整合到一起。在 JSP 中 inlude 标签的作用是在编译时候把目标文件内容拷贝到当前文件中,编译生成一个文件,在运行的时候只有一个文件。(2)类装载器:在目标被装载到 JVM 的时候,由一个特殊的类装载器对目标类的字节码进行增强。(3)运行期:目标对象和切面都是标准的 J
12、ava 类,通过 JVM 的动态代理功能或者 CGLIB 实现在运行时候的动态织入。Servlet 过滤器、Struts2 的拦截器、JSP 中的 指令都可以看作是运行期的织入。下面以日志管理为例介绍 AOP 的作用。传统的日志处理结构如下: 编写日志管理类,用于输出日志信息; 编写业务类,把日志管理类作为成员,每个业务方法中需要使用日志的时候,调用日志管理对象的方法。如果采用 AOP,日志处理的结构如下: 编写业务接口,声明业务方法,模拟存钱和取钱:public interface Bankinterfacepublic void withdraw();public void save();
13、 编写业务类,实现业务接口,业务类中仅仅是业务代码,不再包含任何日志相关的处理代码:public class Bank implements Bankinterfacepublic void withdraw()System.out.println(“正在取钱“);public void save()System.out.println(“正在存钱“); 编写日志管理类:public class LogBankpublic void writeLog(String info)System.out.println(info); 编写执行控制器类,执行控制器类完成的主要功能是把拦截器实例与被代理对
14、象结合起来,在调用具体业务方法之前或之后调用拦截器的功能。在运行的时候,可以通过 java.lang.reflect.Proxy 得到一个代理对象,通过这个代理对象来执行被代理类的业务方法,在被代理类方法被调用的同时,执行处理器会自动调用。用户需要实现java.lang.reflect.InvocationHandler 接口来提供一个执行处理器,也就是在调用业务方法的时候去调用切面功能,相当于建立了被代理类和切面之间的关系。Public class BankProxyHandler implements InvacationHandler/ 被代理对象private Object targe
15、t;/ 创建拦截器实例LogBank log = new LogBank();public Object invoke(Object argo,Method arg1,Object arg2)throws ThrowableObject result = null;/ 拦截 withdraw 方法if(arg1.getName().equals(“withdraw“)log.writelog(“日志信息:开始取款“);result = arg1.invoke(target,arg2);log.writelog(“日志信息:取款结束“);elseresult = arg1.invoke(targ
16、et,arg2);return result;/ 设置代理对象public void setTarget(Object target)this.target = target; 编写代码访问业务方法:/ 创建被代理类对象,也就是真正的业务对象Bank bank = new Bank();/ 创建执行控制类的对象BanProxyHandler bh = new BankProxyHandler(); / 把业务对象注入到执行控制对象中bh.setTarget(obj); / 得到代理类Bankinterface bi =(Bankinterface)Proxy.newProxyInstance(
17、bank.getClass().getClassLoader(),bank.getClass().getInterfaces(),bh);第一个参数是类加载器,第二个参数是得到被代理类的业务接口,第三个参数是执行控制类。然后调用代理类的业务方法即可。bi.withdraw();bi.saving();上面的代码看起来很多,但是大部分代码都可以由系统生成,用户需要做的就是编写日志类、编写业务接口和业务类,其他的功能都可以由系统完成。带来的好处就是在不用在业务方法中硬编码日志这类功能,而是通过配置文件建立两者之间的关系。13.1.3 IoC下面通过账户添加功能的实现来理解 IoC 带来的好处。在没
18、有采用 IoC 的时候的流程: 编写表示账户信息的类 Account:public class Account private Long id;private String firstName;private String lastName;private String email;. / set 方法和 get 方法 编写 Dao 类,模拟对数据库的操作:public class JDBCAccountDao public void save(Account account)System.out.println(“添加帐户信息“); 编写 Action 类,调用 Dao 对象完成对数据的操作
19、:public class AccountAction public void save()/ 构造 Account 对象Account account = new Account();account.setId(new Long(3);account.setFirstName(“san“);account.setLastName(“li“);account.setEmail(““);/ 创建 AccountDao 对象JDBCAccountDao dao = new JDBCAccountDao();dao.save(account); 测试代码:public void basicTest(
20、)AccountAction action = new AccountAction();action.save();分析:在 AccountAction 中需要调用 JDBCAccountDao 的时候,在 save 方法中创建 JDBCAccountDao 对象,然后调用 JDBCAccountDao 对象的 save 方法。在这个应用中 AccountAction 对 JDBCAccountDao 有依赖,并且控制JDBCAccountDao 对象的生成。思考:如果访问数据库的方式发生改变,原来使用 JDBC 的方式(JDBCAccountDao) ,现在要使用 Hibernate(Hib
21、ernateAccountDao) ,如何处理?通常的做法:修改 AccountAction 中的 save 的方法,需要把JDBCAccountDao dao = new JDBCAccountDao();修改为:HibernateAccountDao dao = new HibernateAccountDao();存在的问题:AccountAction 与 JDBCAccountDao 之间的联系太密切。下面进行改进。增加 Dao 接口:public interface AccountDaopublic void save(Account account);修改 JDBCAccountDa
22、o 的代码,实现 AccountDao 接口:public class JDBCAccountDao implements AccountDaopublic void save(Account account)System.out.println(“添加帐户信息“);修改 AccountAction 的代码:public class AccountAction /* 采用注入方式*/private AccountDao dao;public AccountDao getAccountDao() return dao;public void setAccountDao(AccountDao da
23、o) this.dao = dao;public void save() / 构造 Account 对象Account account = new Account();account.setId(new Long(3);account.setFirstName(“san“);account.setLastName(“li“);account.setEmail(““);/ 创建 AccountDao 对象dao.save(account);测试代码:public void dITest()AccountAction action = new AccountAction();/ 实例化 dao 对
24、象AccountDao dao = new JDBCAccountDao();/ 注入 dao 对象action.setAccountDao(dao);/ 调用 save 方法action.save();这时候如果要把 dao 修改为 HibernateAccountDao,只需要让HibernateAccountDao 继承 AccountDao,然后把:AccountDao dao = new JDBCAccountDao();修改为:AccountDao dao = new HibernateAccountDao();AccountAction 中的代码不用修改。解释:这里完成的修改实际
25、上是把 AccountAction 与 JDBCAccountDao 之间的联系解除了。另外把 DAO 的实例化从 AccountAction 中移出,也就是把对DAO 的控制转移到了外面。这也就是控制反转( IoC) ,其实也是依赖注入。另外在这里使用了接口,如果没有这个接口,IoC 或者 DI 是无法完成的。在 Spring 中是如何提供对 IoC 或者 DI 的支持的呢?在 Spring 中提供了一个 IoC 容器,IoC 容器能够完成对应用依赖的对象的实例化,并把实例注入到应用中。用户需要通过配置文件告诉 IoC 容器哪个应用对哪个类有依赖。配置文件:Id 表示对象的名字,class
26、指出类名。property 用于设置属性,accountDao 是属性的名字,但并不是成员变量的名字,而是 setAccountDao 方法去掉 set 之外的部分。ref 指出引用的其他的对象。测试方法:public void springIoCTest()BeanFactory factory = new XmlBeanFactory(new ClassPathResource(“applicationContext.xml“);AccountAction action = (AccountAction)factory.getBean(“accountAction“);action.sav
27、e();BeanFactory 是 Spring 提供的 IoC 容器,管理 Bean 的加载,配置文件采用XML 的形式,所以创建 XmlBeanFactory 的实例, 使用 ClassPathResource 指出资源的位置。13.2 Bean 管理及配置在 Spring 中,被管理的 Bean 都是采用配置文件配置的,下面首先介绍如何在 Spring 中配置和管理 Bean,然后详细介绍如何配置。通常 Spring 管理的 Bean 是业务层的,通常称为 Service,Spring 对 Service管理包括 Service 实例的创建以及 Service 实例之间关系的映射,为了让
28、 Spring对 Service 管理 ,Spring 需要知道 Service,通常是通过配置文件来声明 Service以及 Service 之间的关系。下面的例子是 Spring 与 Struts 整合,管理 Service。步骤 1:添加 Spring 类库支持,添加的类库为 spring.jar。步骤 2:添加 Spring 和 Struts 整合的插件。插件为:struts2-spring-plugin-2.1.8.1.jar该压缩包是 Struts 提供的。步骤 3:添加 Spring 配置文件 applicationContext.xml,在 src 中添加该文件,空的配置文件如
29、下:在 beans 中间添加管理 Bean。步骤 4:修改 web.xml,增加对 Spring 的支持web.xml 代码如下,斜体部分是 spring 相关的配置:contextConfigLocation/WEB-INF/applicationContext*.xml,classpath*:applicationContext*.xml.org.springframework.web.context.ContextLoaderListenercontext-param 元素声明 Spring 的配置文件,可以有多个。 listener 声明一个监听器,加载 Spring 的管理文件。步骤
30、 5:编写测试用的 Service,public class HelloService public String getDate()return new Date().toLocaleString();该 Servlet 只有一个简单的业务方法 getDate(),该方法用于显示当前日期。步骤 6:配置 Service。在 applicationContext.xml 配置 Service,配置后的代码如下:通过元素声明一个 Bean 实例,name 指出创建的对象的名字 ,class指出 Bean 的类型,相当于使用 HelloService 创建了一个实例,helloService 引用
31、指向该实例。步骤 7:在 Action 调用 Service,package test.action;import test.service.HelloService;public class HelloAction private String username;private String time;. / 对 time 和 username 进行操作的 set 方法和 get 方法private HelloService helloService;public HelloService getHelloService() return helloService;public void s
32、etHelloService(HelloService helloService) this.helloService = helloService;public String hello()time = helloService.getDate();return “hello“;在 Action 的 hello 方法中调用了 helloService 的 getDate 方法,虽然在HelloAction 中定义了 helloService,但是并没有给 helloService 赋值,赋值过程是由 Spring 框架完成的。步骤 7:修改测试页面 hello.jsp欢迎界面欢迎$username现在时间是$time用户数为:$numberOfUsers 界面用于输出从 Action 中获取的用户和时间信息。步骤 8:测试,测试结果如图 13-1 所示。图 13-1 测试结果