第一章 Spring之旅
1. 简化Java开发
1.1 Spring是为了解决企业级应用开发的复杂性而创建的。
为了降低Java开发的复杂性,Spring采取了以下4中策略:
· 基于POJO的轻量级和最小侵入性编程
· 通过依赖注入和面向接口实现松耦合
· 基于切面和惯例进行声明式编程
· 通过切面和模板减少样板式代码
1.2 依赖注入(DI)
(1)耦合具有两面性,一方面,紧耦合的代码难以测试、难以复用、难以理解;另一方面,一定程度的耦合又是必须的,因为完全没有耦合的代码什么也做不了。
(2)依赖注入会将所依赖的关系自动交给目标对象,而不是让对象自己去获取依赖;能够让相互协作的组件保持松耦合。
(3)装配(wiring):创建应用组件之间协作的行为。
(4)应用上下文(ApplicationContext):Spring通过应用上下文装载bean的定义并把它们组装起来,应用上下文全权负责对象的创建和组装,Spring自带了多种应用上下文的实现,他们之间的区别仅仅在于如何加载配置。例如可以通过ClassPathXmlApplicationContext类来对XML配置文件进行加载。
1.3 面向切面编程(AOP)
(1)面向切面编程允许你把遍布应用各处的功能分离出来形成可重用的组件。系统由许多不同的组件组成,每一个组件负责各自特定的功能,除了实现自身核心的功能之外,这些组件还经常承担者额外的职责(日志、事务管理、安全等系统服务),这些系统服务通常被称为横切关注点,因为他们会跨越系统的多个组件。例如一个向地址簿增加地址的方法应该只关注如何添加地址,而不应该关注它是不是安全的或者是否需要支持事务。AOP能够使这些服务模块化,并以声明的方式将他们应用到他们需要影响的组件中去,这样各个组件就会具有更高的内聚性并且更加关注自身的业务,完全不需要了解涉及系统服务所带来的复杂性。
(2)AOP应用
自定义一个AOP
1.4 使用模板消除样板式代码
Spring的JdbcTemplate使得执行数据库操作时,避免传统的JDBC样板代码成为可能。
2. 容纳你的Bean
2.1 Spring容器
在基于Spring的应用中,你的应用对象生存与Spring容器中,Spring容器负责创建对象,装配他们,配置他们并管理他们的整个生命周期。容器是Spring框架的核心,容器使用DI管理构成应用的组件,他会创建相互协作的组件之间的关联。Spring容器并非只有一个,自带多个容器实现,可以归纳为两种不同的类型:bean工厂、应用上下文。bean工厂是由org.springframework.beans.factory.BeanFactory接口定义,是最简单的容器,提供基本的DI支持。应用上下文是由org.springframework.context.ApplicationContext接口定义,基于BeanFactory构建,并提供应用框架级别的服务,例如从属性文件解析文本信息,以及发布应用事件给感兴趣的事件监听者。
(1)应用上下文
Spring自带多种类型的应用上下文,如下一些常见的:
· AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文。
· AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文。
· ClassPathXmlApplicationContext:从类路径下的一个或多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类资源。(从所有的类路径,包括jar文件下查找文件资源)
· XmlWebApplicationContext:从Web应用下的一个或多个XML配置文件中加载上下文定义。
· FileSystemXmlApplicationContext:从文件系统中加载应用上下文(从指定的文件系统路径中查找文件资源)。
2.2 bean的生命周期
3. 俯瞰Spring风景线
3.1 Spring模块
(1)Spring核心容器
容器是Spring框架最核心的部分,它管理着Spring应用中bean的创建、配置和管理。在该模块中,包括了Spring bean工厂,它为Spring提供了 DI的功能。基于bean工厂,我们还会发现有多种Spring应用上下文的实现,每一种都提供了配置Spring的不同方式。 除了bean工厂和应用上下文,该模块也提供了许多企业服务,例如E-mail、JNDI访问、EJB集成和调度。 所有的Spring模块都构建于核心容器之上。
(2)Spring的AOP模块
这个模块是Spring应用系统中开发切面的基础。与DI一样,AOP可以帮助应用对象 解耦。借助于AOP,可以将遍布系统的关注点(例如事务和安全)从它们所应用的对象中解耦出来。
(3)数据访问与集成
Spring提供了ORM模块。Spring的ORM模块建立在对DAO(Data Access Object)的支持之上,并为多个ORM框架提供了一种构建DAO的简便方式。Spring没有尝试去创建自己的ORM解决方案,而是对许多流行的ORM框架进行了集成,包括Hibernate、Java Persisternce API、Java Data Object和iBATIS SQL Maps。Spring的事务管理支持所有的ORM框
(4)Web与远程调用
Spring能够与多种流行的MVC框架进行集成,但它的Web和远程调用模块自带了一个强大的MVC框架,有助于在Web层提升应用的松耦合水平。除了面向用户的Web应用,该模块还提供了多种构建与其他应用交互的远程调用方案。Spring远程调用功能集成了RMI(Remote Method Invocation)、Hessian、Burlap、JAX-WS,同时Spring还自带了一个远程调用框架:HTTP invoker。Spring还提供了暴露和使用REST API的良好支持。
(5)Instrumentation
Spring的Instrumentation模块提供了为JVM添加代理(agent)的功能。具体来讲,它为Tomcat提供了一个织入代理,能够为Tomcat传递类文件,就像这些文件是被类加载器加载的一样。
4. Spring的新功能
5. 小结
第二章 装配Bean
1. Spring配置的可选方案
Spring提供了三种主要的装配机制:
· 在XML中进行显式配置。
· 在Java中进行显式配置。
· 隐式的bean发现机制和自动装配。
尽可能地使用自动配置的机制。显式配置越少越好。当你必须要显式配置bean的时候(比如,有些源码不是由你来维护的,而当你需要为这些代码配置bean的时候),我推荐使用类型安全并且比XML更加强大的JavaConfig。最后,只有当你想要使用便利的XML命名空间,并且在JavaConfig中没有同样的实现时,才应该使用XML。
2. 自动化装配bean
Spring从两个角度来实现自动化装配:
· 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean。
· 自动装配(autowiring):Spring自动满足bean之间的依赖。
2.1 创建可被发现的bean
@Component注解:这个注解表明该类会作为组件类,并告知Spring要为这个类创建bean。
@ComponentScan注解:这个注解能够在Spring中启用组件扫描。如果没有其他配置的话,@ComponentScan默认会扫描与配置类相同的包。
注意:组件扫描默认是不启用的。需要显式配置一下Spring,从而命令它去寻找带有@Component注解的类,并为其创建bean。
2.2 为组件扫描的bean命名
Spring应用上下文中所有的bean都会给定一个ID。但Spring会根据类名为其指定一个ID。例如SgtPeppersbean,这个bean所给定的ID为sgtPeppers,也就是将类名的第一个字母变为小写。也可以自定义ID,如下图。
2.3 设置组件扫描的基础包
(1)设置指定的基础包
(2)设置多个基础包
(3)将其指定为包中所包含的类或接口,为basePackageClasses属性所设置的数组中包含了类。这些类所在的包将会作为组件扫描的基础包。
2.4 通过为bean添加注解实现自动装配
(1)自动装配:就是Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean需求的其他bean。为了声明要进行自动装配,我们可以借助Spring的@Autowired注解。
(2)@Autowired注解:不仅能够用在构造器上,还能用在属性的Setter方法上和类的任何方法上。
(3)注意:不管是构造器、Setter方法还是其他的方法,Spring都会尝试满足方法参数上所声明的依赖。假如有且只有一个bean匹配依赖需求的话,那么这个bean将会被装配进来。如果没有匹配的bean,那么在应用上下文创建的时候,Spring会抛出一个异常。为了避免异常的出现,可以将@Autowired的required属性设置为false,将required属性设置为false时,Spring会尝试执行自动装配,但是如果没有匹配的bean的话,Spring将会让这个bean处于未装配的状态。但是,把required属性设置为false时,需要谨慎对待。如果代码中没有进行null检查的话,这个处于未装配状态的属性有可能会出现NullPointerException。如果有多个bean都能满足依赖关系的话,Spring将会抛出一个异常,表明没有明确指定要选择哪个bean进行自动装配。
2.5 验证自动装配
3. 通过Java代码装配bean
通过组件扫描和自动装配实现Spring的自动化配置是更为推荐的方式,但有时候自动化配置的方案行不通,因此需要明确配置Spring。比如说,你想要将第三方库中的组件装配到你的应用中,在这种情况下,是没有办法在它的类上添加@Component和@Autowired注解的,因此就不能使用自动化装配的方案了。这时就必须要采用显式装配的方式,有两种可选方案:Java和XML。JavaConfig是更好的方案,因为它更为强大、类型安全并且对重构友好。JavaConfig是配置代码。它不应该包含任何业务逻辑,JavaConfig也不应该侵入到业务逻辑代码之中。通常会将JavaConfig放到单独的包中,使它与其他的应用程序逻辑分离开来。
3.1 创建配置类
(1)@Configuration注解:表明这个类是一个配置类,该类应该包含在Spring应用上下文中如何创建bean的细节。
3.2 声明简单的bean
(1)@Bean注解会告诉Spring这个方法将会返回一个对象,该对象要注册为Spring应用上下文中的bean。方法体中包含了最终产生bean实例的逻辑。默认情况下,bean的ID与带有@Bean注解的方法名是一样的。
3.3 借助JavaConfig实现注入
(1)通过引用创建bean的方法实现注入
(2)通过调用构造方法实现注入
(3)通过Setter方法实现注入
4. 通过XML装配bean
XML不再是配置Spring的唯一可选方案。Spring现在有了强大的自动化配置和基于Java的配置,XML不应该再是你的第一选择了。
4.1 创建XML配置规范
(1)采用XML配置,要创建一个XML文件,并且要以<beans>元素为根。
4.2 声明一个简单的<bean>
4.3 借助构造器注入初始化bean
(1)使用<constructor-arg>元素,当Spring遇到下图这个<bean>元素时,它会创建一个CDPlayer实例。<constructor-arg>元素会告知Spring要将一个ID为compactDisc的bean引用传递到CDPlayer的构造器中。
(2)使用Spring 3.0所引入的c-命名空间
(3)将字面量注入到构造器中
(4)装配集合,目前,使用c-命名空间的属性无法实现装配集合的功能。
4.4 设置属性
(1)通过Setter方法实现注入
(2)p命名方式
(3)将字面量注入到属性中
(4)util-命名空间:我们不能使用p-命名空间来装配集合,但是可以借助util-list来解决这种情况
5. 导入和混合配置
5.1 在JavaConfig中引用XML配置
5.2 在XML配置中引用JavaConfig
6. 小结
第三章 高级装配
1. 环境与profile
1.1 配置profile bean
(1)在Java中配置:使用@Profile注解指定某个bean属于哪一个profile,当规定的profile激活时,相应的bean才会被创建。没有指定profile的bean始终都会被创建,与激活哪个profile没有关系。
(2)在XML中配置:
1.2 激活profile
Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.active和spring.profiles.default。如果设置了spring.profiles.active属性的话,那么它的值就会用来确定哪个profile是激活的。
但如果没有设置spring.profiles.active属性的话,那Spring将会查找spring.profiles.default的值。如果spring.profiles.active和spring.profiles.default均没有设置的话,那就没有激活的profile,因此只会创建那些没
有定义在 profile中的bean。
有多种方式来设置这两个属性:
》作为DispatcherServlet的初始化参数;
》作为Web应用的上下文参数;
》作为JNDI条目;
》作为环境变量;
》作为JVM的系统属性;
》在集成测试类上,使用@ActiveProfiles注解设置。
(1)在web.xml中设置默认的profile
(2)@ActiveProfiles注解:使用它来指定运行测试时要激活哪个profile。在集成测试时,通常想要激活的是开发环境的profile。例如,下面的测试类片段展现了使用@ActiveProfiles激活dev profile:
2. 条件化的bean
(1)通过Spring4的@Conditional注解来实现条件化创建bean,例如名为MagicBean的类,我们希望只有设置了magic环境属性的时候,Spring才会实例化这个类。
如果环境中没有这个属性,那么MagicBean将会被忽略。设置给@Conditional的类可以是任意实现了Condition接口的类型。
3. 处理自动装配的歧义性
为了阐述自动装配的歧义性,假设我们使用@Autowired注解标注了setDessert()方法,Dessert是一个接口,并且有三个类实现了这个接口,分别为Cake、Cookies和IceCream。
当Spring试图自动装配setDessert()中的Dessert参数时,它并没有唯一、无歧义的可选值。Spring却无法做出选择。Spring此时别无他法,只好宣告失败并抛出异常。Spring会抛
出NoUniqueBeanDefinitionException。当确实发生歧义性的时候,Spring提供了多种可选方案来解决这样的问题。你可以将可选bean中的某一个设为首选(primary)的bean,或
者使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个bean。
3.1 标示首选的bean
(1)在Java中配置首选的bean
(2)在XML中配置首选的bean
(3)注意:如果不止一个bean被设置成了首选bean,那实际上也就是没有首选bean了。
3.2 限定自动装配的bean
(1)@Qualifier注解:是使用限定符的主要方式。它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。
为@Qualifier注解所设置的参数就是想要注入的bean的ID,如果你重构了IceCream类,将其重命名为Gelato的话,bean的ID和默认的限定符会
变为gelato,这就无法匹配setDessert()方法中的限定符。自动装配会失败。这里的问题是setDessert()方法上所指定的限定符与要注入的bean的
名称是紧耦合的。对类名称的任意改动都会导致限定符失效。
(2)创建自定义的限定符
为bean设置自己的限定符,而不是依赖于将bean ID作为限定符。在这里所需要做的就是在bean声明上添加@Qualifier注解,cold限定符分配给了IceCream bean
当通过Java配置显式定义bean的时候,@Qualifier也可以与@Bean注解一起使用。
(3)使用自定义的限定符注解
创建自定义的限定符注解@Cold来代替@Qualifier("cold")
同理创建一个新的@Creamy注解来代替@Qualifier("creamy")
通过自定义的注解将可选范围缩小到只有一个bean满足需求
在默认情况下,Spring应用上下文中所有bean都是作为单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean多少次,每次所注入的都是同一个实例。
如果所使用的类是易变的(mutable),它们会保持一些状态,因此重用是不安全的。在这种情况下,将class声明为单例的bean就不是什么好主意了,因为对象会被污染,稍后重用的时候会出现意想不到的问题。
Spring定义了多种作用域,可以基于这些作用域创建bean:
》单例(Singleton):在整个应用中,只创建bean的一个实例。
》原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
》会话(Session):在Web应用中,为每个会话创建一个bean实例。
》请求(Rquest):在Web应用中,为每个请求创建一个bean实例。
单例是默认的作用域,但是正如之前所述,对于易变的类型,这并不合适。如果选择其他的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。
不管你使用哪种方式来声明原型作用域,每次注入或从Spring应用上下文中检索该bean的时候,都会创建新的实例。这样所导致的结果就是每次操作都能得到自己的Notepad实例。
4.1 使用会话和请求作用域
(1)在典型的电子商务应用中,可能会有一个bean代表用户的购物车。如果购物车是单例的话,那么将会导致所有的用户都会向同一个购物车中添加商品。另一方面,如果购物车是
原型作用域的,那么在应用中某一个地方往购物车中添加商品,在应用的另外一个地方可能就不可用了,因为在这里注入的是另外一个原型作用域的购物车。就购物车bean来说,会话
作用域是最为合适的,因为它与给定的用户关联性最大。要指定会话作用域,我们可以使用@Scope注解,它的使用方式与指定原型作用域是相同的
WebApplicationContext中.SCOPE_SESSION(它的值是session)。这会告诉Spring为Web应用中的每个会话创建一个ShoppingCart。这会创建多个ShoppingCart bean的实例,
但是对于给定的会话只会创建一个实例,在当前会话相关的操作中,这个bean实际上相当于单例的。
ScopedProxyMode.INTERFACES:这个属性解决了将会话或请求作用域的bean注入到单例bean中所遇到的问题。假设我们要将ShoppingCart bean注入到单例StoreService bean的Setter方法中,
因为StoreService是一个单例的bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShoppingCart bean注入到setShoppingCart()方法中。但是ShoppingCart bean是会
话作用域的,此时并不存在。直到某个用户进入系统,创建了会话之后,才会出现ShoppingCart实例。另外,系统中将会有多个ShoppingCart实例:每个用户一个。我们并不想让Spring注入某个固
定的ShoppingCart实例到StoreService中。我们希望的是当StoreService处理购物车功能时,它所使用的ShoppingCart实例恰好是当前会话所对应的那一 个。Spring并不会将实际的ShoppingCart bean
注入到StoreService中,Spring会注入一个到ShoppingCart bean的代理。这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShoppingCart
的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。
proxyMode属性被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。如果ShoppingCart是接口而不是类的话,这是可以的(也是最为理想的代理模式)。
但如果ShoppingCart是一个具体的类,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,
以此来表明要以生成目标类扩展的方式创建代理。
4.2 在XML中声明作用域代理
<aop:scoped-proxy>是与@Scope注解的proxyMode属性功能相同的Spring XML配置元素。它会告诉Spring为bean创建一个作用域代理。默认情况下,它会使用CGLib创建目标类的代理。
但是我们也可以将proxy-target-class属性设置为false,进而要求它生成基于接口的代理:
5. 运行时值注入
当将一个值注入到bean的属性或者构造器参数中时,例如下图为BlankDisc bean设置title和artist,是将值硬编码在配置类中的。
Spring提供了两种在运行时求值的方式:
》属性占位符(Property placeholder)。
》Spring表达式语言(SpEL)
5.1 注入外部的值
(1)在Spring中,处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。
属性文件(app.properties)会加载到Spring的Environment中,可以从这里检索属性。同时,在disc()方法中,会创建一个新的BlankDisc,它的构造器参数是从属性文件中获取的,而这是通过调用getProperty()实现的。
(2)Spring的Environment
getProperty()方法有四个重载的变种形式:
当指定属性不存在的时候,使用一个默认值
获取整形数值
要求属性必须定义,如果disc.title或disc.artist属性没有定义的话,将会抛出IllegalStateException异常
判断某个属性是否存在
将属性解析为类
(3)属性占位符
Spring一直支持将属性定义到外部的属性的文件中,并使用占位符值将其插入到Spring bean中。在Spring装配中,占位符的形式为使用“${ ... }”包装的属性名称。
在XML中按照如下的方式解析BlankDisc构造器参数
为了使用占位符,我们必须要配置一个PropertySourcesPlaceholderConfigurer bean。
5.2 使用Spring表达式语言进行装配
Spring表达式语言(Spring Expression Language,SpEL),能够以一种强大和简洁的方式将值装配到bean属性和构造器参数中,在这个过程中所使用的表达式会在运行时计算得到值。SpEL表达式要放到“#{ ... }”之中
SpEL拥有很多特性,包括:
》使用bean的ID来引用bean;
》调用方法和访问对象的属性;
》对值进行算术、关系和逻辑运算;
》正则表达式匹配;
》集合操作;
(1)引用bean、属性和方法
引用bean:#{ sgtPeppers }
引用bean的属性:#{ sgtPeppers.artist }
引用bean的方法:#{ sgtPeppers.selectArtist() }
(2)在表达式中使用类型
如果要在SpEL中访问类作用域的方法和常量的话,要依赖T()这个关键的运算符,例如 T(java.lang.Math) ,T()运算符的结果会是一个Class对象,T()运算符的真正价值在于它能够访问目标类型的静态方法和常量,例如T(java.lang.Math).PI;T(java.lang.Math).random()
(3)SpEL运算符
示例:有一个bean ID为circle的圆,圆的周长=#{ 2 * T(java.lang.Math).PI * circle.redius};圆的面积=#{ T(java.lang.Math).PI * circle.redius^2 };“^”是用于乘方计算的运算符。
(4)计算正则表达式,matches的运算结果会返回一个Boolean类型的值:如果与正则表达式相匹配,则返回true;否则返回false。
6. 小结
第四章 面向切面的Spring
1. 什么是面向切面编程
1.1 定义AOP术语
(1)描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point)。
(1)通知(Advice)
Spring切面可以应用5种类型的通知:
》前置通知(Before):在目标方法被调用之前调用通知功能;
》后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
》返回通知(After-returning):在目标方法成功执行之后调用通知;
》异常通知(After-throwing):在目标方法抛出异常后调用通知;
》环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
(2)连接点(Join point)
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
(3)切点(Poincut)
切点定义了切面在“何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
(4)切面(Aspect)
切面是“通知”和“切点”的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。
(5)织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。
》编译期:切面在目标类编译时被织入。
》类加载期:切面在目标类加载到JVM时被织入。
》运行期:切面在应用运行的某个时刻被织入。
1.2 Spring对AOP的支持
(1)Spring在运行时通知对象
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。如图4.3所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。
(2)Spring只支持方法级别的连接点
2. 通过切点来选择连接点
2.1 编写切点
2.2 在切点中选择bean,bean()使用bean ID或bean名称作为参数来限制切点只匹配特定的bean。
3. 使用注解创建切面
3.1 定义切面
(1)编写一个切面
(2)AspectJ提供了五个注解来定义通知
3.2 创建环绕通知
ProceedingJoinPoint作为参数。这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint的proceed()方法。需要注意的是,别忘记调用proceed()方法。如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。有可能这就是你想要的效果,但更多的情况是你希望在某个点上执行被通知的方法。你可以不调用proceed()方法,从而阻塞对被通知方法的访问,与之类似,你也可以在通知中对它进行多次调用。要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。
3.3 处理通知中的参数
3.4 通过注解引入新功能
4. 在XML中声明切面
4.1 声明前置和后置通知
4.2 声明环绕通知
4.3 为通知传递参数
4.4 通过切面引入新功能