Spring MVC使用详解

Author Avatar
王彬、张鹏灵、胡伟 7月 02, 2017
  • 在其它设备中阅读本文章

这里先讲述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示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<!-- 在Spring框架中是如何解决从页面传来的字符串的编码问题的呢?
下面我们来看看Spring框架给我们提供过滤器CharacterEncodingFilter
这个过滤器就是针对于每次浏览器请求进行过滤的,然后再其之上添加了父类没有的功能即处理字符编码。
其中encoding用来设置编码格式,forceEncoding用来设置是否理会 request.getCharacterEncoding()方法,
设置为true则强制覆盖之前的编码格式。-->
<filter>
<filter-name>Set Character Encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Set Character Encoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 项目中使用Spring 时,applicationContext.xml配置文件中并没有BeanFactory,要想在业务层中的class 文件中直接引用Spring容器管理的bean可通过以下方式-->
<!--1、在web.xml配置监听器ContextLoaderListener-->
<!--ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。因为它实现了ServletContextListener这个接口,
在web.xml配置这个监听器,启动容器时,就会默认执行它实现的方法。在ContextLoaderListener中关联了ContextLoader这个类,
所以整个加载配置过程由ContextLoader来完成。-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--2、部署applicationContext的xml文件-->
<!--如果在web.xml中不写任何参数配置信息,默认的路径是"/WEB-INF/applicationContext.xml,
在WEB-INF目录下创建的xml文件的名称必须是applicationContext.xml。
如果是要自定义文件名可以在web.xml里加入contextConfigLocation这个context参数:
在<param-value> </param-value>里指定相应的xml文件名,如果有多个xml文件,可以写在一起并以“,”号分隔。
也可以这样applicationContext-*.xml采用通配符,比如这那个目录下有applicationContext-ibatis-base.xml,
applicationContext-action.xml,applicationContext-ibatis-dao.xml等文件,都会一同被载入。
在ContextLoaderListener中关联了ContextLoader这个类,所以整个加载配置过程由ContextLoader来完成。-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext.xml</param-value>
</context-param>
<!--如果你的DispatcherServlet拦截"/",为了实现REST风格,拦截了所有的请求,那么同时对*.js,*.jpg等静态文件的访问也就被拦截了。-->
<!--方案一:激活Tomcat的defaultServlet来处理静态文件-->
<!--要写在DispatcherServlet的前面, 让 defaultServlet先拦截请求,这样请求就不会进入Spring了。-->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.css</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.js</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.ico</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.gif</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.bmp</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.jpg</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.png</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.xml</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.txt</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.map</url-pattern>
</servlet-mapping>
<!--使用Spring MVC,配置DispatcherServlet是第一步。DispatcherServlet是一个Servlet,,所以可以配置多个DispatcherServlet-->
<!--DispatcherServlet是前置控制器,配置在web.xml文件中的。拦截匹配的请求,Servlet拦截匹配规则要自已定义,把拦截下来的请求,依据规则分发到目标Controller来处理。-->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<!--在DispatcherServlet的初始化过程中,框架会在web应用的 WEB-INF文件夹下寻找名为[servlet-name]-servlet.xml 的配置文件,生成文件中定义的bean。-->
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--指明了配置文件的文件名,不使用默认配置文件名,而使用dispatcher-servlet.xml配置文件。-->
<init-param>
<param-name>contextConfigLocation</param-name>
<!--其中<param-value>**.xml</param-value> 这里可以使用多种写法-->
<!--1、不写,使用默认值:/WEB-INF/<servlet-name>-servlet.xml-->
<!--2、<param-value>/WEB-INF/classes/dispatcher-servlet.xml</param-value>-->
<!--3、<param-value>classpath*:dispatcher-servlet.xml</param-value>-->
<!--4、多个值用逗号分隔-->
<param-value>classpath:spring/dispatcher-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup><!--load-on-startup是启动顺序,让这个Servlet随Servletp容器一起启动。-->
</servlet>
<servlet-mapping>
<!--这个Servlet的名字是dispatcher,可以有多个DispatcherServlet,是通过名字来区分的。每一个DispatcherServlet有自己的WebApplicationContext上下文对象。同时保存的ServletContext中和Request对象中.-->
<!--ApplicationContext是Spring的核心,Context我们通常解释为上下文环境,我想用“容器”来表述它更容易理解一些,ApplicationContext则是“应用的容器”了:P,Spring把Bean放在这个容器中,在需要的时候,用getBean方法取出-->
<servlet-name>DispatcherServlet</servlet-name>
<!--Servlet拦截匹配规则可以自已定义,当映射为@RequestMapping("/user/add")时,为例,拦截哪种URL合适?-->
<!--1、拦截*.do、*.htm, 例如:/user/add.do,这是最传统的方式,最简单也最实用。不会导致静态文件(jpg,js,css)被拦截。-->
<!--2、拦截/,例如:/user/add,可以实现现在很流行的REST风格。很多互联网类型的应用很喜欢这种风格的URL。弊端:会导致静态文件(jpg,js,css)被拦截后不能正常显示。 -->
<url-pattern>/</url-pattern> <!--会拦截URL中带“/”的请求。-->
</servlet-mapping>
<welcome-file-list><!--指定欢迎页面-->
<welcome-file>login.html</welcome-file>
</welcome-file-list>
<error-page> <!--当系统出现404错误,跳转到页面nopage.html-->
<error-code>404</error-code>
<location>/nopage.html</location>
</error-page>
<error-page> <!--当系统出现java.lang.NullPointerException,跳转到页面error.html-->
<exception-type>java.lang.NullPointerException</exception-type>
<location>/error.html</location>
</error-page>
<session-config><!--会话超时配置,单位分钟-->
<session-timeout>360</session-timeout>
</session-config>
</web-app>

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

声明应用范围内的初始化参数。

配置:
1
2
3
4
<context-param>
<param-name>xxx</param-name>
<param-value>xxx</param-value>
</context-param>

<context-param>元素含有一对参数名和参数值,用作应用的Servlet上下文初始化参数,参数名在整个Web应用中必须是惟一的,在web应用的整个生命周期中上下文初始化参数都存在,任意的Servlet和jsp都可以随时随地访问它。<param-name>子元素包含有参数名,而<param-value>子元素包含的是参数值。作为选择,可用<description>子元素来描述参数。

使用场景:

比如:定义一个管理员email地址用来从程序发送错误,或者与你整个应用程序有关的其他设置。使用自己定义的设置文件需要额外的代码和管理;直接在你的程序中使用硬编码(Hard-coding)参数值会给你之后修改程序带来麻烦,更困难的是,要根据不同的部署使用不同的设置;通过这种办法,可以让其他开发人员更容易找到相关的参数,因为它是一个用于设置这种参数的标准位置。

2.3.4 filter

过滤器元素将一个名字与一个实现javax.servlet.Filter接口的类相关联。

配置:
1
2
3
4
5
6
7
8
<filter>
<filter-name>xxx</filter-name>
<filter-class>xxx.xxx</filter-class>
<init-param>
<param-name>xxx</param-name>
<param-value>xxx</param-value>
</init-param>
</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相关联。

配置:
1
2
3
4
<filter-mapping>
<filter-name>Set Character Encoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2.3.6 listener

servlet API的版本2.3增加了对事件监听程序的支持,事件监听程序在建立、修改和删除会话或servlet环境时得到通知。Listener元素指出事件监听程序类。

配置:
1
2
3
<listener>
<listener-class>xxx.xxx</listener-class>
</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)内属性的改变。

主要用于监听Web应用事件,其中有两个比较重要的WEB应用事件:应用的启动和停止(starting up or shutting down)和Session的创建和失效(created or destroyed)。应用启动事件发生在应用第一次被Servlet容器装载和启动的时候;停止事件发生在Web应用停止的时候。Session创建事件发生在每次一个新的session创建的时候,类似地Session失效事件发生在每次一个Session失效的时候。为了使用这些Web应用事件做些有用的事情,我们必须创建和使用一些特殊的“监听类”。它们是实现了以下两个接口中任何一个接口的简单java类:javax.servlet.ServletContextListenerjavax.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的实现类,如下所示:

1
2
3
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</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。

配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<servlet>
<servlet-name>ModelViewController</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/spring/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ModelViewController</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
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包下,在这里定义了文档中出现的元素、文档中出现的属性、子元素、子元素的数量、子元素的顺序、元素是否为空、元素和属性的数据类型、元素或属性的默认和固定值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util" xmlns:task="http://www.springframework.org/schema/task"
xmlns:gconfig="http://code.greenline.com/schema/gconfig"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.0.xsd
http://code.greenline.com/schema/gconfig
http://code.greenline.com/schema/gconfig/gconfig.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

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的名字)时,别名尤其有用。

用法:
1
<alias name="fromName" alias="toName"/>

其中,fromName表示同一个容器中原始的bean定义,toName表示这个bean的别名。

举例,子系统A中的配置元数据通过subsystemA-datasource引用一个数据源,子系统B中的配置元数据通过subsystemB-datasource引用这个数据源,使用这两个子系统的主应用通过myApp-datasource引用这个数据源。为了使用这三个名字引用相同的对象,可以在MyApp配置中添加如下的别名定义:

1
2
<alias name="subsystemA-dataSource" alias="subsystemB-dataSource"/>
<alias name="subsystemA-dataSource" alias="myApp-dataSource" />

现在每个子系统都可以通过不同的名字引用这个数据源,并且保证了所有这些子系统引用的都是同一个bean,不会与其它任何子系统的定义冲突,它们还引用了同一个bean。

4.3 实例化bean

使用构造方法实例化

用构造方法创建bean的方式,被开发的类不需要实现任何特定的接口或以特定的形式编码,仅仅指定bean类就足够了。你可以通过无参的构造方法或者有参的构造方法。有参的构造方法有三种方式。

无参的构造方法:

1
<bean id="exampleBean" class="examples.ExampleBean"/>

有参数的构造方法:(第一种:根据参数下标)

1
2
3
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="*****"/>
</bean>

有参数的构造方法:(第一种:根据参数名称)

1
2
3
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="*****" value="*****"/>
</bean>

有参数的构造方法:(第一种:根据参数类型)

1
2
3
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="java.lang.String" value="*****"/>
</bean>

使用静态工厂方法实例化

通过静态工厂方法创建bean需要class属性指定那个包含静态工厂方法的类,并使用factory-method属性指定工厂方法的名字。之后它就像用构造方法创建的对象一样对待。如下,其中createInstance()方法必须是静态的。

1
<bean id="clientService" class="examples.ClientService" factory-method="createInstance"/>

1
2
3
4
5
6
7
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
}
使用实例的工厂方法实例化(非静态)

与静态工厂方法不同的是,使用实例的工厂方法实例化是调用非静态方法来创建。其中,

1、没有class属性。

2、factory-bean为包含工厂方法的那个bean的名字。

3、factory-method表示工厂方法的名字。

如下例:

1
2
<bean id="serviceLocator" class="examples.DefaultServiceLocator"></bean>
<bean id="clientService" factory-bean="serviceLocator" factory-method="createClientServiceInstance"/>

1
2
3
4
5
6
7
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
private DefaultServiceLocator() {}
public ClientService createClientServiceInstance() {
return clientService;
}
}

4.4 依赖

4.4.1 依赖注入

某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在传统的程序设计过程中,通常由调用者主动来创建被调用者的实例。但在Spring里,创建被调用者的工作不再由调用者来完成,因此称为控制反转,创建被调用者实例的工作通常由Spring容器来完成,然后注入调用者,因此也称为依赖注入。Spring容器干两件事情:

1、创建对象,(依赖)。创建某一对象的时候,此时依赖的对象已经创建完毕。

2、将对象装配,也可理解为注入。将创建的对象拿过来,通过构造方法或者setter方法注入,称之为装配。

依赖注入有两种主要的方式,基于构造方法的依赖注入和基于setter方法的依赖注入。

基于构造方法的依赖注入

基于构造方法的依赖注入,由容器调用带有参数的构造方法来完成,每个参数代表一个依赖。

1
2
3
4
5
6
7
8
9
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg>
<ref bean="anotherExampleBean"/>
</constructor-arg>
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg type="int" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

1
2
3
4
5
6
7
8
9
10
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public ExampleBean(AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
this.beanOne = anotherBean;
this.beanTwo = yetAnotherBean;
this.i = i;
}
}

构造方法参数的解决:

构造方法参数在匹配时使用参数的类型,所以有可能会存在歧义。首先看没有歧义的配置,如下:其中Bar和Baz类没有继承关系。

1
2
3
4
5
package x.y;
public class Foo {
public Foo(Bar bar, Baz baz) {
}
}
1
2
3
4
5
6
7
8
<beans>
<bean id="foo" class="x.y.Foo">
<constructor-arg ref="bar"/>
<constructor-arg ref="baz"/>
</bean>
<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
</beans>

1、使用简单类型:比如<value>true</value>,Spring无法判断值的类型,没有类型就没办法匹配。如下:

1
2
3
4
5
6
7
8
9
package examples;
public class ExampleBean {
private int years;
private String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer){
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>

如果使用type属性指定了参数的类型,则容器会使用类型匹配。但是如果不指定类型,spring分不清把7500000赋给year还是42赋给year,因为constructor-arg并不是按它们出现的顺序与参数匹配。

或者使用index属性显式地指定参数的顺序。例如:

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>

2、多个相同类型的参数:

A、同样可以通过索引解决。(索引从0开始);

B、或者使用参数的名字来消除起义。如下:

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
基于setter方法的依赖注入

基于setter方法的依赖注入,由容器在调用无参构造方法或无参静态工厂方法之后调用setter方法来实例化bean。 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}
public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}
public void setIntegerProperty(int i) {
this.i = i;
}
}
1
2
3
4
5
6
7
8
9
<bean id="exampleBean" class="examples.ExampleBean">
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
构造方法和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容器会把这些字符串值转换成参数的实际类型。如下:

1
2
3
4
5
6
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="masterkaoli"/>
</bean>
ref元素

ref能够让一个bean的指定属性引用另一个bean。被引用的bean就称为一个依赖,并且在设置前这个依赖的bean就已经被初始化(如果这个bean是单例的,它可能已经被初始化了)。作用域和检验通过bean、local、parent属性指定的另一个对象的id或name决定。bean是最常用的形式,并且可以引用同一个或父容器中的任何bean。

通过parent属性可以指定对当前容器的父容器中的bean的引用。parent属性的值可能是目标bean的id属性或name属性中的一个,而且目标bean必须在当前容器的父容器中。这种引用形式主要出现在有容器继承并且想把父容器中存在的bean包装成同样名字的代理用于子容器的时候。

1
2
3
4
5
6
7
<bean id="accountService" class="com.foo.SimpleAccountService"></bean>
<bean id="accountService" <!-- bean name is the same as the parent bean -->
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref parent="accountService"/>
</property>
</bean>
集合

<list/>, <set/>, <map/><props/>元素中,可以分别设置Java集合类型List, Set, Map和Properties的属性和参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<bean id="moreComplexObject" class="example.ComplexObject">
Properties:
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.org</prop>
<prop key="support">support@example.org</prop>
<prop key="development">development@example.org</prop>
</props>
</property>
List:
<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
Map:
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
<entry key ="a ref" value-ref="myDataSource"/>
</map>
</property>
Set:
<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>

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实际实例化时才会被发现(可能伴随着致命的错误)。

1
2
3
4
5
6
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean" />
</property>
</bean>

下面是value的方式:

1
2
3
4
<bean id="theTargetBean" class="..." />
<bean id="client" class="...">
<property name="targetName" value="theTargetBean" />
</bean>

idref元素的local属性在4.0版本的xsd中不再支持了,因为它不提供对普通bean的引用。在升级的4.0的schema时只要把已存在的idref local改成idref bean即可。

集合的合并

Spring容器支持集合的合并。包括<list/>、<set/>、 <map/><props/>元素,子类型可以继承父类型集合的值,也可以重写父集合里的值。其实就是交集的结果,重叠的元素会重写。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.com</prop>
<prop key="support">support@example.com</prop>
</props>
</property>
</bean>
<bean id="child" parent="parent">
<property name="adminEmails">
<props merge="true">
<prop key="sales">sales@example.com</prop>
<prop key="support">support@example.co.uk</prop>
</props>
</property>
</bean>
<beans>

需要在子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的类型转换以便元素在被添加到集合之前转换成合适的类型。

1
2
3
4
5
6
public class Foo {
private Map<String, Float> accounts;
public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}

1
2
3
4
5
6
7
8
9
10
11
<beans>
<bean id="foo" class="x.y.Foo">
<property name="accounts">
<map>
<entry key="one" value="9.99"/>
<entry key="two" value="2.75"/>
<entry key="six" value="3.99"/>
</map>
</property>
</bean>
</beans>

当foo的accounts属性准备注入的时候,会通过反射获得强类型Map<String, Float>的泛型信息。然后Spring的类型转换机制识别到value的类型为Float,并把String类型的值9.99,2.75和3.99转换成实际的Float类型。

null和空字符串值

Spring把对属性的空参数作为空字符串。下面的XML片段把email属性的值设置成了空字符串值(“”)。

1
2
3
<bean class="ExampleBean">
<property name="email" value=""/>
</bean>

等价于如下的Java代码:
exampleBean.setEmail("")

也可以通过元素来处理值。如下:

1
2
3
4
5
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>

等价于如下的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命名空间的配置如下:

1
2
3
4
5
6
7
8
9
10
11
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="foo@bar.com"/>
</bean>
<bean name="p-namespace" class="com.example.ExampleBean"
p:email="foo@bar.com"/>
</beans>

p命名空间没有schema定义,所以可以把xml的属性名设置成java的属性名。

属性可以配置两种类型。1、字面值。2、引用。下面列举这两种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="john-classic" class="com.example.Person">
<property name="name" value="John Doe"/>
<property name="spouse" ref="jane"/>
</bean>
<bean name="john-modern"
class="com.example.Person"
p:name="John Doe"1:字面值】
p:spouse-ref="jane"/>【2:引用类型】
<bean name="jane" class="com.example.Person">
<property name="name" value="Jane Doe"/>
</bean>
</beans>

可以看到,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核心包中)。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
<bean id="foo" class="x.y.Foo">【原来xml构造方法的配置方式】
<constructor-arg ref="bar"/>
<constructor-arg ref="baz"/>
<constructor-arg value="foo@bar.com"/>
</bean>
<bean id="foo" class="x.y.Foo" c:bar-ref="bar" c:baz-ref="baz" c:email="foo@bar.com"/>【c命名空间】
</beans>

对于引用类型,c: 命名空间与p: 命名空间使用一样,都通过-ref来引用bean,通过名字设置构造方法的参数。。

某些极少数的情况,如果没法获取构造方法参数名,那么可以使用参数的索引。如下:

1
<bean id="foo" class="x.y.Foo" c:_0-ref="bar" c:_1-ref="baz"/>

经过实践,构造方法的解决机制在匹配参数方面很有效率,所以除非真的需要,推荐在所有配置的地方都使用名字符号。

合成属性名

可以使用合成或嵌套的属性名设置bean的属性,只要路径中除了最后的属性值的所有的组件都不为null,考虑使用如下bean定义:

1
2
3
<bean id="foo" class="foo.Bar">
<property name="fred.bob.sammy" value="123" />
</bean>

foo有一个属性叫fred,fred有一个属性叫bob,bob有一个属性叫sammy,并且最后的sammy属性的值被设置为123。其中,foo的fred属性和fred的bob属性在bean构造后必须不为null,否则将会抛出空指针异常。

内部bean

<property/><constructor-arg/>元素内部定义的bean就是所谓的内部bean。

1
2
3
4
5
6
7
8
<bean id="outer" class="...">
<property name="target">
<bean class="com.example.Person">
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</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的依赖:

1
2
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />

要表示对多个bean的依赖,可以为depends-on属性的值提供多个名字,使用逗号,空格或分号分割:

1
2
3
4
5
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
</bean>
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

depends-on属性能够同时指定初始化时依赖和通信销毁时依赖,这只能在单例bean的情况中运用。依赖的bean在给定的bean销毁之前被销毁。因此,depends-on也能够控制关闭的顺序。(初始化的时候依赖的对象先初始化才能注入,销毁时需要依赖的对象先销毁才能解绑)。

4.4.4 延迟初始化的bean

默认情况下,ApplicationContext的实现创建和配置所有单例bean作为初始化过程的一部分。通常都需要这种预初始化,因为配置或环境中的错误可以立即被发现,相反则可能需要很长时间才能发现错误。如果不需要预初始化,可以延迟初始化。延迟初始化意味着IoC容器在第一次请求到这个bean时才初始化,而不是启动的时候初始化。XML配置如下:

1
2
<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.foo.AnotherBean"/>

当ApplicationContext使用上面的配置启动时时,名为lazy的bean并不会预初始化,而not.lazy则会预初始化。

但是,当一个延迟初始化的bean是另一个非延迟初始化的单例bean的依赖时,ApplicationContext会在启动时创建这个延迟初始化的bean,因为它必须用来满足那个单例的依赖。这个延迟初始化的bean被注入到非延迟初始化的单例bean中。

也可以在<beans/>元素上设置其default-lazy-init属性以便在容器级别控制延迟初始化。

1
<beans default-lazy-init="true"></beans>
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 的实例。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package fiona.apple;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class CommandManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Object process(Map commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

上面这种方式并不推荐使用,因为业务代码跟spring耦合了。方法注入,从某种角度来看是spring IoC容器的高级特性,允许以一种干净的方式处理这种用例。

对于第二种Lookup方法注入,它是容器的一项能力,它可以重写容器管理的bean的方法,并返回另一个bean的查找结果。查找往往涉及到前面描述的那种原型(prototype)bean。spring利用CGLIB库动态地生成字节码子类,从而重写方法以实现查找方法注入。

(1)、为了能使动态的子类有效,被继承的类不能是final,且被重写的方法也不能是final。

(2)、单元测试一个具有抽象方法的类时,需要手动继承此类并重写其抽象方法。

(3)、组件扫描的具体方法也需要具体类。

(4)、一项关键限制是查找方法不能使用工厂方法和配置类中的@Bean方法,因为容器不会在运行时创建一个子类及其实例。

(5)、最后,方法注入的目标对象不能被序列化。

查看前面关于CommandManager类的代码片段,可以发现spring容器会动态生成createCommand()方法的实现。CommandManager不会有任何的spring依赖,如下所示:

1
2
3
4
5
6
7
8
9
package fiona.apple;
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
protected abstract Command createCommand();
}

客户端类包含了将被注入的方法(本例中的CommandManager),被注入的方法需要如下签名:

<public|protected> [abstract] <return-type> theMethodName(no-arguments);

如果方法是抽象的,动态生成的子类会实现这个方法。另外,动态生成的子类也会重写原类中的具体方法。例如:

1
2
3
4
5
<bean id="command" class="fiona.apple.AsyncCommand" scope="prototype">
</bean>
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="command"/>
</bean>

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:

1
2
<bean id="accountService" class="com.foo.DefaultAccountService"/>
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>

4.5.2 原型作用域

非单例的原型作用域的bean将会导致每次请求时都创建一个新的实例。也就是说,在这个bean被注入到另一个bean时,或通过容器的getBean()方法请求它时都会创建一个新的实例。如下图:

在xml形式中,按如下方式定义一个原型bean:

1
<bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/>

4.5.3 依赖于原型bean的单例bean

当使用依赖于原型bean的单例bean的时候,注意依赖关系是在实例化的时候被解决的。因此,如果依赖注入一个原型bean到单例bean,一个新的原型bean会被实例化,然后注入到单例bean中。这个原型实例是唯一一个提供给这个单例bean的实例。但是,假设需要在运行时单例bean每次都获得一个新的原型bean,没有办法做到注入一个新的原型bean到单例bean,因为注入仅仅发生一次,当容器实例化单例bean的时候已经解决了它的依赖的注入。如果需要在运行时获取新的原型实例。

4.5.4 请求、会话、全局会话、应用及WebSocket作用域
请求作用域:

配置如下:

1
<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>

spring容器为每次Http请求创建了一个新的LoginAction的实例。也就是说,loginAction的作用域是Http请求级别的。注解配置如下。

1
2
3
4
@RequestScope
@Component
public class LoginAction {
}

会话作用域

配置如下:

1
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>

spring容器为一个单独的Http会话创建一个新的UserPreferences实例。也就是说userPreferences的作用域是Http会话级别的。注解配置方式如下。

1
2
3
4
@SessionScope
@Component
public class UserPreferences {
}

全局会话作用域

配置如下:

1
<bean id="userPreferences" class="com.foo.UserPreferences" scope="globalSession"/>

全局会话作用域与标准的Http会话作用域类似,不过它只用在基于portlet的web应用上下文中。如果在标准的Servlet应用中定义一个或多个拥有全局会话作用域的bean,那么标准的Http会话作用域将被使用,并不会报错。

应用作用域

配置如下:

1
<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>

这与spring的单例bean很类似,但有两种非常重要的区别:它在每个ServletContext中是单例,而不是spring的“ApplicationContext”(ApplicationContext在任何给定的web应用中可能存在多个)。注解配置如下。

1
2
3
4
@ApplicationScope
@Component
public class AppPreferences {
}

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耦合:

1
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>

1
2
3
4
public class ExampleBean {
public void init() {
}
}

上面这种方式严格来说与下面的方式是一样的:

1
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>

1
2
3
4
public class AnotherExampleBean implements InitializingBean {
public void afterPropertiesSet() {
}
}
销毁回调

实现了org.springframework.beans.factory.DisposableBean接口的bean允许在包含这的容器被销毁时获取一个回调。

不建议使用DisposableBean回调接口,同样会把代码与spring耦合。可以使用@PreDestroy注解或在bean定义中指定销毁方法作为替代方案。在xml配置中,可以使用元素的destroy-method属性。在Java配置中,可以使用@Bean的destroyMethod属性。例如,下面这种方式不会与spring耦合:

1
<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>

1
2
3
4
public class ExampleBean {
public void cleanup() {
}
}

上面这种方式严格来说与下面是一样的:

1
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>

1
2
3
4
5
public class AnotherExampleBean implements DisposableBean {
public void destroy() {
// do some destruction work (like releasing pooled connections)
}
}
默认的初始化和销毁方法

当不使用spring指定的InitializingBean和DisposableBean回调接口编写初始化和销毁回调方法时,我们一般会把这些方法命名为init(), initialize(), dispose()等等。

可以配置spring容器在每个bean上自动寻找初始化和销毁的回调方法。开发者可以直接使用init()作为初始化方法而不用为每一个bean配置init-method=”init”了。容器会在bean创建后调用这个方法(而且,这与标准的生命周期回调是一致的)。这个特性也需要为初始化和销毁方法定义统一的命名约定。如下:

1
2
3
4
5
6
7
8
9
10
11
public class DefaultBlogService implements BlogService {
private BlogDao blogDao;
public void setBlogDao(BlogDao blogDao) {
this.blogDao = blogDao;
}
public void init() {
if (this.blogDao == null) {
throw new IllegalStateException("The [blogDao] property must be set.");
}
}
}

1
2
3
4
5
<beans default-init-method="init">
<bean id="blogService" class="com.foo.DefaultBlogService">
<property name="blogDao" ref="blogDao" />
</bean>
</beans>

顶级元素<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。

1
2
3
4
5
6
7
8
9
10
11
12
<bean id="inheritedTestBean" abstract="true"
class="org.springframework.beans.TestBean">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<bean id="inheritsWithDifferentClass"
class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBean" init-method="initialize">
<property name="name" value="override"/>
<!-- the age property value of 1 will be inherited from parent -->
</bean>

如果没有指定子定义的class,则会使用父定义的class,不过也可以重写它。后者必须保证子定义中的class与父定义中的class兼容,即它可以接受父定义中的属性值。

子定义可以从父定义继承作用域、构造方法参数值、属性值以及方法,也可以为它们添加新值。任何在子定义中定义的作用域、初始化方法、销毁方法或者静态工厂方法都会覆盖父定义中的配置。

其余的配置总是取决于子定义:依赖、自动装配模式、依赖检查、单例、延迟初始化。

上面的例子中在父定义中显式地使用了abstract属性。如果父定义不指定类,那么必须显式地标记abstract属性,如下:

1
2
3
4
5
6
7
8
9
10
<bean id="inheritedTestBeanWithoutClass" abstract="true">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBeanWithoutClass" init-method="initialize">
<property name="name" value="override"/>
<!-- age will inherit the value of 1 from the parent bean definition-->
</bean>

父bean不能被实例化,因为它本身并不完整,并且被显式地标记为abstract了。当一个bean被定义为abstract,则它只能用做定义子bean的模板。如果试着使用abstract的父bean,比如通过另一个bean引用它或调用getBean()方法获取它,都会报错。同样,容器内部的preInstantiateSingletons()方法也会忽略定义为abstract的bean。