跟着《Spring实战》彻底系统地学习一下Spring。
对Spring的新认识
Spring的关键
- 基于POJO的轻量级和最小侵入性编程。
- 通过DI和面向接口实现松耦合。
- 基于切面和惯例进行声明式编程。
- 通过切面和模板减少样板式代码。
所谓侵入性,就是指很多框架要求使用者继承它们的类或实现它们的接口,这样应用程序就和框架绑定死了,Spring的侵入性很小。
注意,DI或者说IoC绝不是Spring框架的特权,Java就有一个JDI规范,很多有DI框架的功能都可以去实现它。
在单元测试时,可以直接用mock框架(如Mockito)去创建接口的mock实现,注入进去做测试。
Spring容器
Spring容器不止一个,Spring自带了多个容器实现,常见的有bean工厂:
org.springframework.beans.factory.BeanFactory
该接口是最简单的容器,提供基本的DI支持。
最广泛使用的是应用上下文:
org.springframework.context.ApplicationContext
它基于bean工厂,提供了应用框架级别的服务。
ApplicationContext的几种实现
不同的实现,主要区别在于装载应用上下文的渠道不同。
- AnnotationConfigApplicationContext:从基于注解的配置类中加载上下文。
- AnnotationConfigWebApplicationContext:从基于注解的配置类中加载Spring Web上下文。
- ClassPathXmlApplicationContext:从类加载路径下的xml配置文件中加载上下文。
- FileSystemXmlApplicationContext:从文件系统中的xml配置文件加载上下文。
- XmlWebApplicationContext:从web应用下的xml配置文件中加载Spring Web上下文。
其中第二个和第六个是和Spring的Web应用有关的,不在Spring核心容器的范畴内。
装配bean的三种方式
创建应用对象之间协作关系的行为称为装配(wiring),在类中表现为组合,所以依赖的装配也就是DI的目的。
[1]自动装配
关键:组件扫描、自动装配。
标识要被创建bean的类
给类加上@Component
注解,表示该类是一个组件类,Spring会为这个类创建bean,并将创建的bean(对象)纳入Spring容器管理。
@Component
注解可以被JDI规范的@Named
注解代替,他们功能基本一样。
组件类生成的bean默认id是类名第一个字母小写,可以在注解中显式指明id:
@Component(value = "lzh")
public class SbLzh {
//...
}
启用组件扫描
[1]JavaConfig方式
使用@Configuration
注解,表示该类是一个配置类;再使用@ComponentScan
注解,默认扫描与该配置类相同的包及其子包,以发现组件类。
扫描某个包及其子包,将这”某个包”称为基础包。
如果想扫描其它包而不是默认的所在包,或者想指定多个扫描的基础包,可以在@ComponentScan
注解上指定:
@ComponentScan(basePackages = {"org.lzh", "org.sb"})
这种方式是类型不安全的,因为这些基础包是用String指定的,没法在编译期检查其是否正确(存在)。可以用这种方式:
@ComponentScan(basePackageClasses = {LzhConfig.class, SbConfig.class})
这里所指定的类或者接口所在的包将作为扫描的基础包。不过程序中的类经常可能会变的,所以不妨额外设置空标记接口,仅仅用来标识所在包:
@ComponentScan(basePackageClasses = {LzhMark.class, SbMark.class})
[2]XML方式
在xml文件中添加context命名空间,使用:
<context:component-scan base-package="org.lzh"/>
开启组件扫描,并指明要在哪个包及其子包下扫描组件类。
为bean添加注解实现自动装配
使用Spring的@Autowired
或者JDI的@Inject
注解,添加到属性上时,不必提供setter方法就会按类型寻找合适的bean自动注入;添加到方法上时,Spring都会尝试满足方法参数上所声明的依赖,不论是什么方法。
当没有找到合适的bean来注入时,会抛出异常,可以使用:
@Autowired(required = false)
说明这个自动注入不是必须的,如果找不到就算了(null)。
[2]通过JavaConfig装配
JavaConfig是配置代码,不应该侵入到业务逻辑代码中,通常将JavaConfig放到单独的包中。使用@Configuration
就创建了一个配置类。
声明bean的方法
在方法上使用@Bean
注解来声明一个bean:
//@Configuration指明是一个JavaConfig配置类
@Configuration
public class LzhConfig {
//@Bean修饰的方法用来声明一个bean
@Bean
//返回值使用了抽象接口BookService(面向抽象),也可以面向具体
//方法名bkSrvc将成为bean的id
//方法内将对bean进行组装,最后返回一个具体的实现类对象即组装好的bean
public BookService bkSrvc() {
BookService bookService = new BookServiceImp();
//组装这个bean...
//返回组装好的bean对象
return bookService;
}
}
组装bean的细节
前面那个例子里的bean什么依赖都没有,如果一个bean依赖另一个bean,在装配时最简单的方式就是引用配置类中创建bean的方法,如:
@Configuration
public class LzhConfig {
@Bean
public Book book() {
return new Book();
}
@Bean
public BookService bkSrvc() {
//引用配置类中创建bean的方法
BookService bookService = new BookServiceImp(book());
return bookService;
}
}
注意这种方式和直接new一个传进去的区别!Spring中的bean默认都是单例的,对于添加了@Bean
注解的方法,Spring会拦截所有对它的调用,并确保直接返回该方法所创建的单例bean,而不是每次都真的调用它!
另一种更好的方式是,从@Bean
方法的参数传入bean:
@Bean
public BookService bkSrvc(Book bk) {
BookService bookService = new BookServiceImp(bk);
return bookService;
}
这种方式既能保证单例bean,又不要求该bean的声明同在该配置类中。这个bean可以通过组件扫描自动发现,也可以通过XML来配置,也可以声明在任何一个配置类中。
JavaConfig往往是最好的选择
JavaConfig是装配bean的最灵活的方式,因为它本质就是一个Java类,只受到Java语言的限制。Java毕竟是图灵完备的,所以这种方式几乎没有功能上的限制。
通过XML装配
这种方式已经用很多次了,只记录一些新学到的知识。
bean的id
用<bean>
元素声明一个bean,如果没有给出id,那么默认的id就是类名#计数
,如:
<bean class="org.lzh.model.Book"/>
<bean class="org.lzh.model.Book"/>
相当于:
<bean class="org.lzh.model.Book" id="org.lzh.model.Book#0"/>
<bean class="org.lzh.model.Book" id="org.lzh.model.Book#1"/>
在使用时为了简洁,尽量只对那些需要将它ref注入到其它bean中的bean明确指明id。
使用合适的IDE
XML配置的一大缺点就是,像class这种属性也只能用字符串,所以XML配置不能从编译期的类型检查中受益,可以用能够感知Spring功能的IDE(如STS和IDEA)来解决这个问题(编码时就能检查出来)。
c-命名空间
Spring的c-
命名空间可在一定程度上代替<constructor-arg>
为构造器注入bean,如:
<bean class="org.lzh.service.imp.BookServiceImp">
<constructor-arg ref="org.lzh.model.Book#0"/>
</bean>
可以替换为:
<bean class="org.lzh.service.imp.BookServiceImp" c:book-ref="org.lzh.model.Book#0"/>
其中,c:
后跟的book是构造器参数的名称,-ref
表示是注入引用而不是字面量。如果不想使用构造器参数名称,也可以使用参数的索引,即c:_索引号
配合-ref
:
<bean class="org.lzh.service.imp.BookServiceImp" c:_0-ref="org.lzh.model.Book#0"/>
装配集合类型
当要把集合装配到构造器参数中时,c-
命名空间做不了,只能用<constructor-arg>
元素。
注意<set>
和<list>
用来装配集合时没什么区别,都可以用来装配List、Set和数组。区别主要是在装配到哪种集合类型里,如果是Set那么会忽略重复值。
<set>
和<list>
使用子元素<ref bean="..."/>
来实现引用集;使用<value>...</value>
(里面的东西不需要双引号)来实现字面量集。
p-命名空间
Spring的p-
命名空间可在一定程度上代替<property>
使用setter注入bean,如:
<bean class="org.lzh.service.imp.BookServiceImp">
<property name="book" ref="org.lzh.model.Book#0"/>
</bean>
可以替换为:
<bean class="org.lzh.service.imp.BookServiceImp" p:book-ref="org.lzh.model.Book#0"/>
当注入字面量时同样是把-ref
去掉就行。
配置导入
在前面学习使用参数注入到@Bean
修饰的方法中实现注入时,虽然不要求该bean的声明同在该配置类中,但只要不是自动发现bean,就需要手动导入配置,才能使用导入的配置中所配置的bean。
因此,两种手动配置的方式——JavaConfig和XML之间必定存在相互导入的方式,下面逐个列举。
JavaConfig中导入JavaConfig
使用@Import
注解:
@Configuration
@Import(value = {SbConfig.class, CatConfig.class})
public class LzhConfig {
@Bean
public BookService bkSrvc(Book bk) { //这里使用SbConfig中配置的Book类的bean
BookService bookService = new BookServiceImp(bk);
return bookService;
}
}
JavaConfig中导入XML
使用@ImportResource
注解:
@Configuration
@ImportResource(value = {"classpath:CatContext.xml", "classpath:SbContext.xml"})
public class LzhConfig {
@Bean
public BookService bkSrvc(Book bk) { //这里使用CatContext.xml中配置的Book类的bean
BookService bookService = new BookServiceImp(bk);
return bookService;
}
}
XML中导入XML
使用<import>
元素:
<import resource="classpath:CatContext.xml"/>
XML中导入JavaConfig
使用<bean>
元素:
<bean class="org.cat.CatConfig"/>
不管使用哪种手动配置方式,不管如何导来导去,通常都应创建一个根配置,导入所有的JavaConfig和XML,在之前实习的时候就发现公司系统里也都是这样做的。