Spring MVC使用详解
这里先讲述spring MVC使用详解,主要介绍web.xml、spring MVC常用注解标签、Bean概述。
1 简介
Spring Web MVC是一种基于Java的实现了Web MVC设计模式的请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将web层进行职责解耦,基于请求驱动指的就是使用请求-响应模型,框架的目的就是帮助我们简化开发。
这里先讲述spring MVC使用详解,主要介绍web.xml、spring MVC常用注解标签、Bean概述。
2 web.xml详解
2.1 web.xml示例
|
|
2.2 加载过程
1、启动web项目时候,容器首先会去它的配置文件web.xml读取两个节点: <listener></listener>和<context-param></context-param>
。
2、容器创建一个ServletContext(application),这个WEB项目所有部分都将共享这个上下文。
3、容器以<context-param></context-param>
的name作为键,value作为值,将其转化为键值对,存入ServletContext。
4、容器创建<listener></listener>
中的类实例,根据配置的class类路径<listener-class>
来创建监听,在监听中会有contextInitialized(ServletContextEvent args)
初始化方法,启动Web应用时,系统调用Listener的该方法,在该方法中获得:
ServletContext application = ServletContextEvent.getServletContext();
context-param(value) = application.getInitParameter("context-param(key)");
得到这个context-param的值之后,可以做一些操作了。
5、容器读取<filter></filter>
,根据指定的类路径来实例化过滤器。
以上都是在WEB项目还没有完全启动起来的时候就已经完成了的工作。如果系统中有Servlet,则Servlet是在第一次发起请求的时候被实例化的,而且一般不会被容器销毁,它可以服务于多个用户的请求。所以,Servlet的初始化都要比上面提到的那几个要迟;
总的来说,web.xml的加载顺序是:<context-param>-> <listener> -> <filter> -> <servlet>
其中,如果web.xml中出现了相同的元素,则按照在配置文件中出现的先后顺序来加载;
对于某类元素而言,与它们出现的顺序是有关的。以<filter>
为例,web.xml中当然可以定义多个<filter>
,与<filter>
相关的一个元素是<filter-mapping>
,对于拥有相同<filter-name>
的<filter>
和<filter-mapping>
元素而言,<filter-mapping>
必须出现在<filter>
之后,否则当解析到<filter-mapping>
时,它所对应的<filter-name>
还未定义。web容器启动初始化每个<filter>
时,按照<filter>
出现的顺序来初始化的,当请求资源匹配多个<filter-mapping>
时,<filter>
拦截资源是按照<filter-mapping>
元素出现的顺序来依次调用doFilter()
方法的。<servlet>
同<filter>
类似。
2.3标签详解
2.3.1 display-name
定义了WEB应用的名字
2.3.2 description
声明WEB应用的描述信息
2.3.3 context-param
声明应用范围内的初始化参数。
配置:
|
|
<context-param>
元素含有一对参数名和参数值,用作应用的Servlet上下文初始化参数,参数名在整个Web应用中必须是惟一的,在web应用的整个生命周期中上下文初始化参数都存在,任意的Servlet和jsp都可以随时随地访问它。<param-name>
子元素包含有参数名,而<param-value>
子元素包含的是参数值。作为选择,可用<description>
子元素来描述参数。
使用场景:
比如:定义一个管理员email地址用来从程序发送错误,或者与你整个应用程序有关的其他设置。使用自己定义的设置文件需要额外的代码和管理;直接在你的程序中使用硬编码(Hard-coding)参数值会给你之后修改程序带来麻烦,更困难的是,要根据不同的部署使用不同的设置;通过这种办法,可以让其他开发人员更容易找到相关的参数,因为它是一个用于设置这种参数的标准位置。
2.3.4 filter
过滤器元素将一个名字与一个实现javax.servlet.Filter
接口的类相关联。
配置:
|
|
Filter介绍:
Filter可认为是Servle的一种“加强版”,主要用于对用户请求request进行预处理,也可以对Response进行后处理,是个典型的处理链。使用Filter的完整流程是:Filter对用户请求进行预处理,接着将请求HttpServletRequest交给Servlet进行处理并生成响应,最后Filter再对服务器响应HttpServletResponse进行后处理。Filter与Servlet具有完全相同的生命周期,且Filter也可以通过<init-param>
来配置初始化参数,获取Filter的初始化参数则使用FilterConfig的getInitParameter()
。换种说法,Servlet里有request和response两个对象,Filter能够在一个request到达Servlet之前预处理request,也可以在离开Servlet时处理response,Filter其实是一个Servlet链。Filter的一些常见应用场合包括:认证、日志和审核、图片转换、数据压缩、密码、令牌、触发资源访问事件、XSLT、媒体类型链等。Filter可负责拦截多个请求或响应,一个请求或响应也可被多个Filter拦截。
Filter必须实现javax.servlet.Filter
接口,在该接口中定义了三个方法:
(1)、void init(FilterConfig config)
:用于完成Filter的初始化。FilteConfig用于访问Filter的配置信息。
(2)、void destroy()
:用于Filter销毁前,完成某些资源的回收。
(3)、void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
:实现过滤功能的核心方法,该方法就是对每个请求及响应增加额外的处理。该方法实现对用户请求request进行预处理,也可以实现对服务器响应response进行后处理,它们的分界线为是否调用了chain.doFilter(request,response)
,执行该方法之前,即对用户请求request进行预处理,执行该方法之后,即对服务器响应response进行后处理。
Filter配置:
Filter配置与Servlet的配置非常相似,需要配置两部分:配置Filter名称和Filter拦截器URL模式。区别在于Servlet通常只配置一个URL,而Filter可以同时配置多个请求的URL。配置Filter有两种方式:
(1)、在Filter类中通过Annotation进行配置。
(2)、在web.xml文件中通过配置文件进行配置。
我们使用的是web.xml这种配置方式,下面重点介绍<filter>
内包含的一些元素。
<filter>
用于指定Web容器中的过滤器,可包含<filter-name>、<filter-class>、<init-param>、<icon>、<display-name>、<description>
。
<filter-name>
用来定义过滤器的名称,该名称在整个程序中都必须唯一。
<filter-class>
元素指定过滤器类的完全限定的名称,即Filter的实现类。
<init-param>
为Filter配置参数,与<context-param>
具有相同的元素描述符<param-name>
和<param-value>
。
<filter-mapping>
元素用来声明Web应用中的过滤器映射,过滤器被映射到一个servlet或一个URL 模式。这个过滤器的<filter>
和<filter-mapping>
必须具有相同的<filter-name>
,指定该Filter所拦截的URL。过滤是按照部署描述符的<filter-mapping>
出现的顺序执行的。
2.3.5 filter-mapping
一旦命名了一个过滤器,就要利用filter-mapping元素把它与一个或多个servlet相关联。
配置:
|
|
2.3.6 listener
servlet API的版本2.3增加了对事件监听程序的支持,事件监听程序在建立、修改和删除会话或servlet环境时得到通知。Listener元素指出事件监听程序类。
配置:
|
|
listener介绍:
web应用程序定义监听器,监听器用来监听各种事件,比如:application和session事件,所有的监听器按照相同的方式定义,功能取决去它们各自实现的接口,常用的Web事件接口有如下几个:
(1)ServletContextListener:用于监听Web应用的启动和关闭;
(2)ServletContextAttributeListener:用于监听ServletContext范围(application)内属性的改变;
(3)ServletRequestListener:用于监听用户的请求;
(4)ServletRequestAttributeListener:用于监听ServletRequest范围(request)内属性的改变;
(5)HttpSessionListener:用于监听用户session的开始和结束;
(6)HttpSessionAttributeListener:用于监听HttpSession范围(session)内属性的改变。
javax.servlet.ServletContextListener
或javax.servlet.http.HttpSessionListener
,如果想让你的类监听应用的启动和停止事件,你就得实现ServletContextListener接口;想让你的类去监听Session的创建和失效事件,那你就得实现HttpSessionListener接口;
listener配置:
配置Listener只要向Web应用注册Listener实现类即可,无序配置参数之类的东西,因为Listener获取的是Web应用ServletContext(application)的配置参数。为Web应用配置Listener的两种方式:
(1)使用@WebListener修饰Listener实现类即可。
(2)在web.xml文档中使用<listener>
进行配置。
我们选择web.xml这种配置方式,只有一个元素<listener-class>
指定Listener的实现类,如下所示:
这里的<listener>
用于Spring的加载,Spring加载可以利用ServletContextListener实现,也可以采用load-on-startup Servlet实现,但是当<filter>
需要用到bean时,但加载顺序是:先加载<filter>
后加载<servlet>
,则<filter>
中初始化操作中的bean为null;所以,如果过滤器中要使用到bean,此时就可以根据加载顺序<listener> -> <filter> -> <servlet>
,将spring的加载改成Listener的方式。
2.3.7 servlet
在向servlet或JSP页面制定初始化参数或定制URL时,必须首先命名servlet。
配置:
|
|
Servlet介绍:
Servlet通常称为服务器端小程序,是运行在服务器端的程序,用于处理及响应客户的请求。Servlet是个特殊的java类,继承于HttpServlet。客户端通常只有GET和POST两种请求方式,Servlet为了响应则两种请求,必须重写doGet()
和doPost()
方法。大部分时候,Servlet对于所有的请求响应都是完全一样的,此时只需要重写service()
方法即可响应客户端的所有请求。另外HttpServlet有两个方法:
(1)init(ServletConfig config)
:创建Servlet实例时,调用该方法的初始化Servlet资源。
(2)destroy()
:销毁Servlet实例时,自动调用该方法的回收资源。
通常无需重写init()
和destroy()
两个方法,除非需要在初始化Servlet时,完成某些资源初始化的方法,才考虑重写init()
方法,如果重写了init()
方法,应在重写该方法的第一行调用super.init(config)
,该方法将调用HttpServlet的init()
方法。如果需要在销毁Servlet之前,先完成某些资源的回收,比如关闭数据库连接,才需要重写destory()
方法。
Servlet的生命周期:创建Servlet实例有两个时机:客户端第一次请求某个Servlet时,系统创建该Servlet的实例,大部分Servlet都是这种Servlet。Web应用启动时立即创建Servlet实例,即load-on-start Servlet。每个Servlet的运行都遵循如下生命周期:
(1)创建Servlet实例。
(2)Web容器调用Servlet的init()方法,对Servlet进行初始化。
(3)Servlet初始化后,将一直存在于容器中,用于响应客户端请求,如果客户端发送GET请求,容器调用Servlet的doGet()方法处理并响应请求;如果客户端发送POST请求,容器调用Servlet的doPost()
方法处理并响应请求。或者统一使用service()
方法处理来响应用户请求。
(4)Web容器决定销毁Servlet时,先调用Servlet的destory()
方法,通常在关闭Web应用时销毁Servlet实例。
Servlet配置:
为了让Servlet能响应用户请求,还必须将Servlet配置在web应用中,配置Servlet需要修改web.xml文件。从Servlet3.0开始,配置Servlet有两种方式:
(1)在Servlet类中使用@WebServlet Annotation进行配置。
(2)在web.xml文件中进行配置。
我们用web.xml文件来配置Servlet,需要配置<servlet>
和<servlet-mapping>
。<servlet>
用来声明一个Servlet。<icon>、<display-name>
和<description>
元素的用法和<filter>
的用法相同。<init-param>
元素与<context-param>
元素具有相同的元素描述符,可以使用<init-param>
子元素将初始化参数名和参数值传递给Servlet,访问Servlet配置参数通过ServletConfig对象来完成,ServletConfig提供如下方法:java.lang.String.getInitParameter(java.lang.String name)
:用于获取初始化参数。ServletConfig获取配置参数的方法和ServletContext获取配置参数的方法完全一样,只是ServletConfig是取得当前Servlet的配置参数,而ServletContext是获取整个Web应用的配置参数。
3 Spring MVC注解标签详解
3.1 @Controller
在SpringMVC中,控制器Controller负责处理由DispatcherServlet分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model,然后再把该Model返回给对应的View进行展示。在SpringMVC中提供了一个非常简便的定义Controller的方法,你无需继承特定的类或实现特定的接口,只需使用@Controller标记一个类是Controller,然后使用@RequestMapping 和@RequestParam等一些注解用以定义URL 请求和Controller方法之间的映射,这样的Controller就能被外界访问到。此外Controller不会直接依赖于HttpServletRequest和HttpServletResponse等HttpServlet对象,它们可以通过Controller的方法参数灵活的获取到。
@Controller用于标记在一个类上,使用它标记的类就是一个SpringMVC Controller对象。分发处理器将会扫描使用了该注解的类的方法,并检测该方法是否使用了@RequestMapping注解。@Controller只是定义了一个控制器类,而使用@RequestMapping注解的方法才是真正处理请求的处理器。单单使用@Controller标记在一个类上还不能真正意义上的说它就是SpringMVC的一个控制器类,因为这个时候Spring还不认识它。那么要如何做Spring才能认识它呢?这个时候就需要我们把这个控制器类交给Spring 来管理。有两种方式:
(1)在SpringMVC的配置文件中定义MyController的bean对象。
(2)在SpringMVC的配置文件中告诉Spring该到哪里去找标记为@Controller的Controller控制器。
3.2 @RequestMapping
一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径,属性如下:
A)、value, method
value:指定请求的实际地址,指定的地址可以是URI Template 模式(后面将会说明);
Method:指定请求的method类型, GET、POST、PUT、DELETE等;
B)、consumes,produces
consume:指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;
produces:指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回
C)、params,headers
params:指定request中必须包含某些参数值是,才让该方法处理。
headers:指定request中必须包含某些指定的header值,才能让该方法处理请求。
3.3 @Resource和@Autowired
@Autowired和@Resource都是做bean的注入时使用,其实@Resource并不是Spring的注解,它的包是javax.annotation.Resource,需要导入,但是Spring支持该注解的注入
共同点:
两者都可以写在字段和setter方法上。两者如果都写在字段上,那么就不需要再写setter方法。
不同点
(1)@Autowired为Spring提供的注解,只按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。如果我们想使用按照名称(byName)来装配,可以结合@Qualifier注解一起使用;
(2)@Resource默认按照ByName自动注入,由J2EE提供,需要导入包javax.annotation.Resource。@Resource有两个重要的属性:name和type,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以,如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略。
@Resource装配顺序:
(1)如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。
(2)如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。
(3)如果指定了type,则从上下文中找到类似匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。
(4)如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。
3.4 @PathVariable
用于将请求URL中的模板变量映射到功能处理方法的参数上,即取出uri模板中的变量作为参数。
3.5 @requestParam
@requestParam用于在SpringMVC后台控制层获取参数,类似一种是request.getParameter("name")
,它有三个常用参数:defaultValue = "0", required = false, value = "isApp"
;defaultValue 表示设置默认值,required 铜过boolean设置是否是必须要传入的参数,value 值表示接受的传入的参数类型。
3.6 @ResponseBody
该注解用于将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区,使用时机返回的数据不是html标签的页面,而是其他某种格式的数据时(如json、xml等)使用。
3.7 @RequestBody
注解常用来处理Content-Type:application/json, application/xml等请求参数是请求体。
3.8 @ModelAttribute和 @SessionAttributes
该Controller的所有方法在调用前,先执行此@ModelAttribute方法,可用于注解和方法参数中,可以把这个@ModelAttribute特性,应用在BaseController当中,所有的Controller继承BaseController,即可实现在调用Controller时,先执行@ModelAttribute方法,@SessionAttributes即将值放到session作用域中,写在class上面。
3.9 @Component
相当于通用的注解,当不知道一些类归到哪个层时使用,但是不建议使用。
3.10 @Repository
用于注解dao层,在daoImpl类上面注解。
3.11 @RestController:
经常见到一些控制器实现了REST的API,只为服务于JSON,XML或其它自定义的类型内容,@RestController用来创建REST类型的控制器,与@Controller类型。@RestController就是这样一种类型,它避免了你重复的写@RequestMapping与@ResponseBody。
4 Bean概述
4.1 命名空间
ns:
namespace,表示命名空间。
xsd文件:
定义了它所控制的spring配置文件能够写的具体内容。所以说这个xsd文件就是这个spring配置文件的命名空间。Xmlns表示当前配置文件的namespace其中之一,以“xmlns:context=”http://www.springframework.org/schema/context"”这个为例,它具体发xsd文件可以通过点击“http://www.springframework.org/schema/context/spring-context-3.0.xsd”进入,是在spring的spring-aop的jar包下,在这里定义了文档中出现的元素、文档中出现的属性、子元素、子元素的数量、子元素的顺序、元素是否为空、元素和属性的数据类型、元素或属性的默认和固定值。
Xsd文件跟xml文件的关系:
xsd是xml的语法,所以把xsd称之为元数据,一个xml的语法可以由好几个xsd来确定,早期的xml语法是dtd文件,新的语法是xsd文件,一是据将来的条件可扩展,二是比DTD丰富和有用,三是用XML书写,四是支持数据类型,五是支持命名空间。
Schema:
一个xsd文件称之为schema,所以beans.xml中有schemalocation,那为什么是以http开头的呢,需要schema文件互相之间不冲突,总之要起一个独一无二的地址或者名字,所以http://www.springframework.org/schema/context
这个表示一个key,一个标识,一个标识意味着独一无二,所以用http网站作为标识是最容易避免冲突的方法,所以标识一般都是以http开头。
关于xsd相关的内容可以参考下面的博文:
(https://www.ibm.com/developerworks/cn/xml/x-1008dubb/)
4.2 别名(alias)
场景:
在大型软件系统中,可能会有很多子系统的配置,而所有的子系统都有它自己配套的bean,而在你的系统扩展时不希望触及这些原始bean定义(或者不能操作这些bean的名字)时,别名尤其有用。
用法:
|
|
其中,fromName表示同一个容器中原始的bean定义,toName表示这个bean的别名。
举例,子系统A中的配置元数据通过subsystemA-datasource引用一个数据源,子系统B中的配置元数据通过subsystemB-datasource引用这个数据源,使用这两个子系统的主应用通过myApp-datasource引用这个数据源。为了使用这三个名字引用相同的对象,可以在MyApp配置中添加如下的别名定义:
|
|
现在每个子系统都可以通过不同的名字引用这个数据源,并且保证了所有这些子系统引用的都是同一个bean,不会与其它任何子系统的定义冲突,它们还引用了同一个bean。
4.3 实例化bean
使用构造方法实例化
用构造方法创建bean的方式,被开发的类不需要实现任何特定的接口或以特定的形式编码,仅仅指定bean类就足够了。你可以通过无参的构造方法或者有参的构造方法。有参的构造方法有三种方式。
无参的构造方法:
有参数的构造方法:(第一种:根据参数下标)
有参数的构造方法:(第一种:根据参数名称)
有参数的构造方法:(第一种:根据参数类型)
使用静态工厂方法实例化
通过静态工厂方法创建bean需要class属性指定那个包含静态工厂方法的类,并使用factory-method属性指定工厂方法的名字。之后它就像用构造方法创建的对象一样对待。如下,其中createInstance()
方法必须是静态的。
|
|
使用实例的工厂方法实例化(非静态)
与静态工厂方法不同的是,使用实例的工厂方法实例化是调用非静态方法来创建。其中,
1、没有class属性。
2、factory-bean为包含工厂方法的那个bean的名字。
3、factory-method表示工厂方法的名字。
如下例:
|
|
4.4 依赖
4.4.1 依赖注入
某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在传统的程序设计过程中,通常由调用者主动来创建被调用者的实例。但在Spring里,创建被调用者的工作不再由调用者来完成,因此称为控制反转,创建被调用者实例的工作通常由Spring容器来完成,然后注入调用者,因此也称为依赖注入。Spring容器干两件事情:
1、创建对象,(依赖)。创建某一对象的时候,此时依赖的对象已经创建完毕。
2、将对象装配,也可理解为注入。将创建的对象拿过来,通过构造方法或者setter方法注入,称之为装配。
依赖注入有两种主要的方式,基于构造方法的依赖注入和基于setter方法的依赖注入。
基于构造方法的依赖注入
基于构造方法的依赖注入,由容器调用带有参数的构造方法来完成,每个参数代表一个依赖。
|
|
构造方法参数的解决:
构造方法参数在匹配时使用参数的类型,所以有可能会存在歧义。首先看没有歧义的配置,如下:其中Bar和Baz类没有继承关系。
|
|
|
|
1、使用简单类型:比如<value>true</value>
,Spring无法判断值的类型,没有类型就没办法匹配。如下:
|
|
|
|
如果使用type属性指定了参数的类型,则容器会使用类型匹配。但是如果不指定类型,spring分不清把7500000赋给year还是42赋给year,因为constructor-arg并不是按它们出现的顺序与参数匹配。
或者使用index属性显式地指定参数的顺序。例如:
|
|
2、多个相同类型的参数:
A、同样可以通过索引解决。(索引从0开始);
B、或者使用参数的名字来消除起义。如下:
|
|
基于setter方法的依赖注入
基于setter方法的依赖注入,由容器在调用无参构造方法或无参静态工厂方法之后调用setter方法来实例化bean。 如下:
|
|
|
|
构造方法和setter方法的选择
setter方法:
(1)、因为方法可以命名,所以setter方法注入在描述性上要比构造方法注入好一些。
(2)、对于复杂的依赖关系,如果采用构造注入,会导致构造器国语臃肿,难以阅读。Spring在创建Bean实例时,需要同时实例化器依赖的全部实例,因而导致性能下降。而使用设值注入,则能避免这些问题。尤其是在某些属性可选的情况下,多参数的构造器更加笨重。
构造注入:
(1)、它会使应用程序的组件实现为不可变的对象,并保证必需的依赖不为null;setter方法仅仅只用于可选依赖,这些可选依赖应该在类中被赋值合理的默认值。否则,在使用这项依赖的任何地方都要做非null检查
(2)、构造注入可以在构造器中决定依赖关系的注入顺序,优先依赖的优先注入。例如,组件中其他依赖关系的注入,常常要依赖于DataSrouce的注入。采用构造注入,可以在代码中清晰的决定注入顺序。
(3)、依赖关系只能在构造器中设定,则只有组件的创建者才能改变组件的依赖关系。对组件的调用者而言,组件内部的依赖关系完全透明,更符合高内聚的原则。
(4)、对于依赖关系不需要变化的Bean,构造注入更有用处。因为没有setter方法,所有的依赖关系全部在构造器内设定,因此,不需要担心后续的代码对依赖关系产生破坏。
(5)、如果一个第三方类没有暴露任何setter方法,那么只能选择构造方法注入了。
4.4.2 依赖与配置详解
直接设置值
<property/>
的value属性可以为属性或构造方法参数指定字符串形式的值。Spring容器会把这些字符串值转换成参数的实际类型。如下:
|
|
ref元素
ref能够让一个bean的指定属性引用另一个bean。被引用的bean就称为一个依赖,并且在设置前这个依赖的bean就已经被初始化(如果这个bean是单例的,它可能已经被初始化了)。作用域和检验通过bean、local、parent属性指定的另一个对象的id或name决定。bean是最常用的形式,并且可以引用同一个或父容器中的任何bean。
通过parent属性可以指定对当前容器的父容器中的bean的引用。parent属性的值可能是目标bean的id属性或name属性中的一个,而且目标bean必须在当前容器的父容器中。这种引用形式主要出现在有容器继承并且想把父容器中存在的bean包装成同样名字的代理用于子容器的时候。
|
|
集合
在<list/>, <set/>, <map/>
和<props/>
元素中,可以分别设置Java集合类型List, Set, Map和Properties的属性和参数。
|
|
map的键值或set的值也可以是以下任何元素:
bean、ref、idref、 list、set、map、props、value、null
idref元素
idref元素可以获取spring容器中的bean的name的值(一个字符串),而不是bean的实例。idref元素的功能与<value>
类似,只是idref增加了验证的功能,减少配置的书写错误机率。除了<idref bean=""/>
,如果被引用的bean在同一个xml文件中,且bean的名字就是bean的id,除了可以使用<idfef local=""/>
,此属性允许xml解析器在解析XML的时候对引用的bean进行验证。而value方式,传给client bean的targetName属性值并没有被验证。任何的输入错误仅在client bean实际实例化时才会被发现(可能伴随着致命的错误)。
下面是value的方式:
idref元素的local属性在4.0版本的xsd中不再支持了,因为它不提供对普通bean的引用。在升级的4.0的schema时只要把已存在的idref local改成idref bean即可。
集合的合并
Spring容器支持集合的合并。包括<list/>、<set/>、 <map/>
或<props/>
元素,子类型可以继承父类型集合的值,也可以重写父集合里的值。其实就是交集的结果,重叠的元素会重写。如下:
|
|
需要在子bean的adminEmails属性上的props元素的merge=true。当child被实例化时,它拥有一个adminEmails Properties集合,这个集合包含了父子两个集合adminEmails合并的结果。
administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk
其中<list/>, <map/>
和<set/>
集合的合并也是类似的。在<list/>
元素合并的时候,因为List集合维护的是有序的值,所以父集合的值在子集合值的前面。在Map, Set和Properties中的值则不存在顺序。没有顺序的集合类型更有效,因此容器内部更倾向于使用Map, Set和Properties。
集合合并的局限性:不能合并不同的集合类型(比如,Map和List),如果试图这么做将会抛出异常。merge属性必须指定在子集合的定义上,在父集合上指定merge属性将是多余的且不会合并集合。
强类型集合
Java5引入了泛型类型,所以可以使用强类型的集合。也就是说,例如,可以声明一个只能包含String元素的集合类型。如果使用Spring把强类型的集合注入到一个bean,可以利用Spring的类型转换以便元素在被添加到集合之前转换成合适的类型。
|
|
当foo的accounts属性准备注入的时候,会通过反射获得强类型Map<String, Float>
的泛型信息。然后Spring的类型转换机制识别到value的类型为Float,并把String类型的值9.99,2.75和3.99转换成实际的Float类型。
null和空字符串值
Spring把对属性的空参数作为空字符串。下面的XML片段把email属性的值设置成了空字符串值(“”)。
等价于如下的Java代码:exampleBean.setEmail("")
也可以通过
等价于如下的Java代码:exampleBean.setEmail(null)
使用p命名空间简写XML
使用<property>
元素为Bean的属性装配值和引用并不太复杂。尽管如此,Spring的命名空间p 提供了另一种Bean 属性的装配方式,该方式不需要配置如此多的尖括号。p命名空间使你可以使用bean元素的属性,而不是内置的<property/>
元素,来描述属性值和合作的bean。
Spring支持使用命名空间扩展配置,这是基于XML的schema定义的。本章讨论的bean的配置形式是定义在XML schema文档中的。然而,p命名空间不是定义在XSD文件中的,它存在于Spring的核心包中。标准的XML形式和p命名空间的配置如下:
p命名空间没有schema定义,所以可以把xml的属性名设置成java的属性名。
属性可以配置两种类型。1、字面值。2、引用。下面列举这两种方式:
可以看到,p:name="John Doe"
使用p命名空间来定义属性值,p:spouse-ref="jane"
使用p命名空间来定义属性的引用,它等同于<property name=”spouse” ref=”jane”/>
凭借-ref表明这不是直接的值而是对另一个对象的引用。
p命名空间并没有标准的XML形式灵活。例如,属性如果以Ref结尾则会与属性的引用冲突,标准的XML形式就不会有这样的问题。我们推荐仔细地选择自己的方式,并和团队成员交流,以避免XML文档中同时出现三种不同的方式。
使用c命名空间简写XML
与使用p命名空间简写XML类似,c命名空间是Spring3.1新引入的,允许使用内联属性配置构造方法的参数,而不用嵌套constructor-arg元素。同样c命名空间也没有定义在XSD文件中(但是存在于Spring核心包中)。例子如下:
对于引用类型,c: 命名空间与p: 命名空间使用一样,都通过-ref来引用bean,通过名字设置构造方法的参数。。
某些极少数的情况,如果没法获取构造方法参数名,那么可以使用参数的索引。如下:
经过实践,构造方法的解决机制在匹配参数方面很有效率,所以除非真的需要,推荐在所有配置的地方都使用名字符号。
合成属性名
可以使用合成或嵌套的属性名设置bean的属性,只要路径中除了最后的属性值的所有的组件都不为null,考虑使用如下bean定义:
foo有一个属性叫fred,fred有一个属性叫bob,bob有一个属性叫sammy,并且最后的sammy属性的值被设置为123。其中,foo的fred属性和fred的bob属性在bean构造后必须不为null,否则将会抛出空指针异常。
内部bean
在<property/>
或<constructor-arg/>
元素内部定义的bean就是所谓的内部bean。
|
|
内部bean不需要定义id或name,即使指定了,容器也不会使用它作为标识符。容器在创建内部bean时也会忽略其作用域标志,内部bean总是匿名的且总是随着外部bean一起创建。不可能把内部bean注入到除封闭bean以外的合作bean,也不可能单独访问它们。
4.4.3 使用depends-on
如果一个bean是另一个bean的依赖,那说明这个bean是另一个bean的属性。在XML配置中使用<ref/>
元素来完成这件事。但是,有时bean之间的依赖并不是直接的,例如,一个静态的初始化器需要被触发,比如在数据库驱动注册时。depends-on属性可以明确地强制一个或多个bean在使用它(们)的bean初始化之前被初始化。
下面的例子使用depends-on属性表示对一个单例bean的依赖:
要表示对多个bean的依赖,可以为depends-on属性的值提供多个名字,使用逗号,空格或分号分割:
depends-on属性能够同时指定初始化时依赖和通信销毁时依赖,这只能在单例bean的情况中运用。依赖的bean在给定的bean销毁之前被销毁。因此,depends-on也能够控制关闭的顺序。(初始化的时候依赖的对象先初始化才能注入,销毁时需要依赖的对象先销毁才能解绑)。
4.4.4 延迟初始化的bean
默认情况下,ApplicationContext的实现创建和配置所有单例bean作为初始化过程的一部分。通常都需要这种预初始化,因为配置或环境中的错误可以立即被发现,相反则可能需要很长时间才能发现错误。如果不需要预初始化,可以延迟初始化。延迟初始化意味着IoC容器在第一次请求到这个bean时才初始化,而不是启动的时候初始化。XML配置如下:
|
|
当ApplicationContext使用上面的配置启动时时,名为lazy的bean并不会预初始化,而not.lazy则会预初始化。
但是,当一个延迟初始化的bean是另一个非延迟初始化的单例bean的依赖时,ApplicationContext会在启动时创建这个延迟初始化的bean,因为它必须用来满足那个单例的依赖。这个延迟初始化的bean被注入到非延迟初始化的单例bean中。
也可以在<beans/>
元素上设置其default-lazy-init属性以便在容器级别控制延迟初始化。
|
|
4.4.5 自动装配(不建议使用)
spring IoC容器可以自动装配相互协作之间的bean,粒度可以控制到单个bean。自动装配有以下优点:
(1)、自动装配将极大地减少指定属性或构造方法参数的需要。
(2)、自动装配可以更新配置当你的对象进化时。例如,如果你需要为一个类添加一个依赖,那么不需要修改配置就可以自动满足。因此,自动装配在开发期间非常有用,但不否定在代码库变得更稳定的时候切换到显式的装配。
自动装配的功能有四种模式,如下所示:
no:(默认)不自动装配。bean的引用必须通过ref元素定义。对于大型项目,不推荐更改默认设置,因为显式地指定合作者能够更好地控制且更清晰。
byName:按属性名称自动装配。Spring根据属性寻找名称相同的bean。如有一个属性叫(它有set(…)方法),Spring会找到一个名叫*的bean并把它设置到这个属性中。
byType:按属性的类型自动装配。
(1)、必须只存在一个bean。
(2)、如果多个bean名字相同,会抛出异常。
(3)、如果一个都没有,则什么事都不会发生,这个属性不会被装配。
constructor:与按类型装配类似,只不过用于构造方法的参数。如果这个构造方法的参数类型在容器中不存在明确的一个bean,将会抛出异常。
自动装配的局限性和缺点:
如果一个项目一直使用自动装配,它会运行得很好。如果只是用在一两个bean上,则会出现混乱。自动装配有如下局限性和缺点:
(1)、在property和constructor-arg上显式设置的依赖总是覆盖自动装配。而且,不能自动装配所谓的简单属性,如基本类型、String和Classes(也包括简单属性的数组)。
(2)、自动装配没有显式装配精确。
(3)、从Spring容器生成文档的工具可能找不到装配信息。
(4)、容器中的多个bean定义可能会匹配setter方法或构造方法参数指定的类型。对于数组、集合或Map,这未必是个问题。但是,对于那些需要单个值的依赖,这种歧义并不能随意解决。如果没有各自不同的bean定义,将会抛出异常。
在上述场景中,有如下几种选择:
(1)、放弃自动装配,改为显式地装配。
(2)、设置bean的autowire-candidate属性为false以避免自动装配。
(3)、设置<bean/>
标签的primary属性为true,从而为其指定一个单独的bean作为主要的候选者。
避免bean自动装配:
(1)、单个bean:设置<bean>
元素的autowire-candidate属性为false。
(2)、autowire-candidate属性:如果某个Bean不想被自动装配到其他的Bean中,Spring中的Bean有个autowire-candidate属性设置为false就可以达到我们的目的。
4.4.6 方法注入
在大部分情况下,容器中的bean都是singleton类型的。如果一个singleton bean要引用另外一个singleton bean,或者一个非singleton bean要引用另外一个非singleton bean时,通常情况下将一个bean定义为另一个bean的property值就可以了。不过对于具有不同生命周期的bean来说这样做就会有问题了,比如在调用一个singleton类型bean A的某个方法时,需要引用另一个非singleton(prototype)类型的bean B,对于bean A来说,容器只会创建一次,这样就没法在需要的时候每次让容器为bean A提供一个新的的bean B实例。
对于上面的问题Spring提供了三种解决方案:
(1)、放弃控制反转(不推荐)。通过实现ApplicationContextAware接口让bean A能够感知bean 容器,并且在需要的时候通过使用getBean("B")
方式向容器请求一个新的bean B实例。
(2)、Lookup方法注入。Lookup方法注入利用了容器的覆盖受容器管理的bean方法的能力,从而返回指定名字的bean实例。
(3)、自定义方法的替代方案。该注入能使用bean的另一个方法实现去替换自定义的方法。
对于第一种放弃控制反转可以通过实现ApplicationContextAware接口使 A 能连接到容器,并在每次 A 需要 B 的实例的时候调用容器的getBean(“B”)
方法取得新的 B 的实例。如下:
上面这种方式并不推荐使用,因为业务代码跟spring耦合了。方法注入,从某种角度来看是spring IoC容器的高级特性,允许以一种干净的方式处理这种用例。
对于第二种Lookup方法注入,它是容器的一项能力,它可以重写容器管理的bean的方法,并返回另一个bean的查找结果。查找往往涉及到前面描述的那种原型(prototype)bean。spring利用CGLIB库动态地生成字节码子类,从而重写方法以实现查找方法注入。
(1)、为了能使动态的子类有效,被继承的类不能是final,且被重写的方法也不能是final。
(2)、单元测试一个具有抽象方法的类时,需要手动继承此类并重写其抽象方法。
(3)、组件扫描的具体方法也需要具体类。
(4)、一项关键限制是查找方法不能使用工厂方法和配置类中的@Bean方法,因为容器不会在运行时创建一个子类及其实例。
(5)、最后,方法注入的目标对象不能被序列化。
查看前面关于CommandManager类的代码片段,可以发现spring容器会动态生成createCommand()方法的实现。CommandManager不会有任何的spring依赖,如下所示:
客户端类包含了将被注入的方法(本例中的CommandManager),被注入的方法需要如下签名:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
如果方法是抽象的,动态生成的子类会实现这个方法。另外,动态生成的子类也会重写原类中的具体方法。例如:
commandManager这个bean会在任何它需要command实例的时候调用其createCommand()
方法。如果实际需要,则必须把command声明为原型(prototype)。如果被声明为单例(singleton),则每次返回的都是同一个command实例。
4.5 bean的作用域
Bean的作用域以下几类:
singleton(单例):(默认值),每个spring的IoC容器中只保留一份bean定义对应的实例。
prototype(原型):一份bean定义对应多个实例。
request(请求):依赖于Http请求的生命周期,也就是说,每个Http请求都有它自己实例。这只在web应用上下文中有效。
session(会话):依赖于Http会话的生命周期。这只在web应用上下文中有效。
globalSession(全局会话):依赖于全局Http会话的生命周期。典型地仅当使用在Portlet上下文中有效。这只在web应用上下文中有效。
application(应用):依赖于ServletContext的生命周期。这只在web应用上下文中有效。
websocket 依赖于WebSocket的生命周期。这只在web应用上下文中有效。
下面的作用域都是可以直接使用的,也可以创建自定义作用域。
4.5.1 单例作用域
一个单例bean仅共享同一个实例,并且所有的请求都只返回同一个实例。
也就是说,spring容器严格地只创建那个bean定义的一个实例。这个单独的实例被存储在单例bean的缓存中,并且所有后来的请求及引用都会返回这个缓存的对象。
在xml形式中,按如下方式定义一个单例bean:
4.5.2 原型作用域
非单例的原型作用域的bean将会导致每次请求时都创建一个新的实例。也就是说,在这个bean被注入到另一个bean时,或通过容器的getBean()
方法请求它时都会创建一个新的实例。如下图:
在xml形式中,按如下方式定义一个原型bean:
4.5.3 依赖于原型bean的单例bean
当使用依赖于原型bean的单例bean的时候,注意依赖关系是在实例化的时候被解决的。因此,如果依赖注入一个原型bean到单例bean,一个新的原型bean会被实例化,然后注入到单例bean中。这个原型实例是唯一一个提供给这个单例bean的实例。但是,假设需要在运行时单例bean每次都获得一个新的原型bean,没有办法做到注入一个新的原型bean到单例bean,因为注入仅仅发生一次,当容器实例化单例bean的时候已经解决了它的依赖的注入。如果需要在运行时获取新的原型实例。
4.5.4 请求、会话、全局会话、应用及WebSocket作用域
请求作用域:
配置如下:
spring容器为每次Http请求创建了一个新的LoginAction的实例。也就是说,loginAction的作用域是Http请求级别的。注解配置如下。
会话作用域
配置如下:
spring容器为一个单独的Http会话创建一个新的UserPreferences实例。也就是说userPreferences的作用域是Http会话级别的。注解配置方式如下。
全局会话作用域
配置如下:
全局会话作用域与标准的Http会话作用域类似,不过它只用在基于portlet的web应用上下文中。如果在标准的Servlet应用中定义一个或多个拥有全局会话作用域的bean,那么标准的Http会话作用域将被使用,并不会报错。
应用作用域
配置如下:
这与spring的单例bean很类似,但有两种非常重要的区别:它在每个ServletContext中是单例,而不是spring的“ApplicationContext”(ApplicationContext在任何给定的web应用中可能存在多个)。注解配置如下。
4.6 bean的特性(生命周期)
为了与容器中bean的生命周期管理交互,可以实现Spring的InitializingBean和DisposableBean接口。容器会在初始化和销毁bean时调用前者的afterPropertiesSet()
和后者destroy()
方法执行相应的动作。
spring内部使用BeanPostProcessor的实例处理它能找到的任何回调接口并调用合适的方法。如果需要定制一些spring未提供的又可以立即使用的功能或生命周期行为,可以直接实现自己的BeanPostProcessor 。
初始化回调
org.springframework.beans.factory.InitializingBean
接口允许bean的所有必要属性都被容器设置好后执行初始化操作。
不建议使用InitializingBean 接口,因为这样会把代码与spring耦合起来。可以使用@PostConstruct注解或在bean定义中指定初始化方法作为替代方案。在xml配置中,可以使用init-method属性指定一个无参的方法。在Java配置中,可以使用@Bean的initMethod属性。例如,下面这种方式不会与spring耦合:
|
|
上面这种方式严格来说与下面的方式是一样的:
|
|
销毁回调
实现了org.springframework.beans.factory.DisposableBean接口的bean允许在包含这的容器被销毁时获取一个回调。
不建议使用DisposableBean回调接口,同样会把代码与spring耦合。可以使用@PreDestroy注解或在bean定义中指定销毁方法作为替代方案。在xml配置中,可以使用
|
|
上面这种方式严格来说与下面是一样的:
|
|
默认的初始化和销毁方法
当不使用spring指定的InitializingBean和DisposableBean回调接口编写初始化和销毁回调方法时,我们一般会把这些方法命名为init(), initialize(), dispose()
等等。
可以配置spring容器在每个bean上自动寻找初始化和销毁的回调方法。开发者可以直接使用init()作为初始化方法而不用为每一个bean配置init-method=”init”
了。容器会在bean创建后调用这个方法(而且,这与标准的生命周期回调是一致的)。这个特性也需要为初始化和销毁方法定义统一的命名约定。如下:
|
|
顶级元素<beans/>
上的default-init-method属性会使得spring容器自动识别bean中的init方法作为初始化回调方法。当bean创建并装配后,如果它有这个方法就会在合适的时候被调用。
同样地,可以配置顶级元素<beans/>
上的default-destroy-method属性声明销毁的回调方法。
当bean存在与上述约定不一样的回调方法名称时,可以使用<bean/>
元素本身的init-method和destroy-method代替默认的方法。
spring容器保证在bean的所有依赖都注入完毕时立马调用初始化回调方法。因此,初始化方法是调用在原生(非原生即代理)的bean引用上,这意味着AOP的拦截器还没开始生效。目标bean首先被完全创建,然后AOP代理及其拦截器链才被应用。如果目标bean与代理是分开定义的,我们甚至可以绕过代理直接与原生bean进行交互。因此,在初始化方法上应用拦截器是矛盾的,因为,这样做会把目标bean的生命周期与它的代理或拦截器耦合在一起,在直接使用原生bean时就会有很奇怪的语法。
4.7 bean定义的继承
bean的定义中可以包含大量的配置信息,包括构造方法参数、属性值以及容器指定的信息,比如初始化方法、静态工厂方法名等等。子bean定义可以从父定义中继承配置信息。子定义可以重写这些值,也可以在需要的时候添加新的。使用父子定义可以少码几个字,这是模板的一种形式。
如果编程式地使用ApplicationContext,子定义可以使用ChildBeanDefinition类。不过大部分用户并不这样使用,而是以类似ClassPathXmlApplicationContext的形式声明式地配置。在xml中,可以使用子定义的parent属性指定父bean。
如果没有指定子定义的class,则会使用父定义的class,不过也可以重写它。后者必须保证子定义中的class与父定义中的class兼容,即它可以接受父定义中的属性值。
子定义可以从父定义继承作用域、构造方法参数值、属性值以及方法,也可以为它们添加新值。任何在子定义中定义的作用域、初始化方法、销毁方法或者静态工厂方法都会覆盖父定义中的配置。
其余的配置总是取决于子定义:依赖、自动装配模式、依赖检查、单例、延迟初始化。
上面的例子中在父定义中显式地使用了abstract属性。如果父定义不指定类,那么必须显式地标记abstract属性,如下:
父bean不能被实例化,因为它本身并不完整,并且被显式地标记为abstract了。当一个bean被定义为abstract,则它只能用做定义子bean的模板。如果试着使用abstract的父bean,比如通过另一个bean引用它或调用getBean()
方法获取它,都会报错。同样,容器内部的preInstantiateSingletons()
方法也会忽略定义为abstract的bean。