基於註解的Spring
- 一.Web.xml 配置
- Web容器加載Spring配置
<!-- Spring application*.xml資源 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath*:/Spring/applicationContext*.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
- Spring MVC配置
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/SpringMVC/applicationContext-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>*.jhtml</url-pattern>
</servlet-mapping>
- 處理由JavaBeans Introspector的使用而引起的緩衝泄露的配置
<listener>
<listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>
</listener>
- 解決亂碼的配置
<filter>
<filter-name>encodingFilter</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>
<!-- 會影響到response中的字符編碼 -->
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter>
<filter-name>encodingConvertFilter</filter-name>
<filter-class>net.sage.filter.EncodingConvertFilter</filter-class>
<init-param>
<param-name>fromEncoding</param-name>
<param-value>ISO-8859-1</param-value>
</init-param>
<init-param>
<param-name>toEncoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>*.jhtml</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>encodingConvertFilter</filter-name>
<url-pattern>*.jhtml</url-pattern>
</filter-mapping>
- 使用Spring時Web.xml的完整配置
<!-- Spring application*.xml資源 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath*:/applicationContext*.xml
</param-value>
</context-param>
<filter>
<filter-name>encodingFilter</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>
<filter-name>encodingConvertFilter</filter-name>
<filter-class>net.sage.filter.EncodingConvertFilter</filter-class>
<init-param>
<param-name>fromEncoding</param-name>
<param-value>ISO-8859-1</param-value>
</init-param>
<init-param>
<param-name>toEncoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>*.jhtml</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>encodingConvertFilter</filter-name>
<url-pattern>*.jhtml</url-pattern>
</filter-mapping>
<!-- Spring MVC -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/applicationContext-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>*.jhtml</url-pattern>
</servlet-mapping>
<!-- Spring 加載application*.xml -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 處理由JavaBeans Introspector的使用而引起的緩衝泄露 -->
<listener>
<listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>
</listener>
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<error-page>
<error-code>404</error-code>
<location>/common/resource_not_found.jhtml</location>
</error-page>
- 二.Application.xml 配置
- 基於Spring註解定義Bean的配置
- 1.添加命名空間
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="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">
- 2.掃描類包以應用註解定義Bean
通常如此配置:
<context:component-scan base-package="com.sage"/>
如果僅希望掃描特定的類,而非基包下的所有類,可以使用resource-pattern屬性過濾出特定的類:
<!-- 基包爲com.sage,默認情況下resource-pattern屬性的值爲”**/*.class”,即基包中的所有類。這裏設置爲”anno/*.clss”,Spring僅會掃描基包裏anno子包中的類 -->
<context:component-scan base-package="com.sage" resource-pattern="anno/*.class"/>
如果使用resource-pattern屬性滿足不你的要求,可以使用<context:component-scan>的過濾子元素實現更靈活的過濾:
<context:component-scan base-package="com.sage">
<context:include-filter type=”regex” expression="com\.sage\.anno\..*" />
<context:exclude-filter type=aspectj expression="com.sage..*Controller+" />
</context:component-scan>
過濾表達式
類別 |
示例 |
說明 |
annotation |
com.sage.XxxAnnotation |
所有標註了XxxAnnotation的類。該類型採用目標類是否標註了某個註解進行過濾 |
assignable |
com.sage.XxxService |
所有繼承或擴展了XxxService的類。該類型採購目標類是否繼承或擴展某個特定類進行過濾。 |
aspectj |
Com.sage..*Service+ |
所有類名以Service結束的類及繼承或擴展它們的類。該類型採購AspectJ表達式進行過濾。 |
regex |
Com\.sage\.anno\..* |
所有com.sage.anno類包下的類。該類型採購正則表達式根據目標類名進行過濾。 |
Custom |
com.sage.XxxTypeFilter |
採購XxxTypeFilter通過代碼的代工根據過濾規則。該類必須實現org.springframework.core.type.TypeFilter接口。 |
<context:component-scan /> 在application.xml中可以配置多個。
- 3.最終配置
<?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:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="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">
<context:component-scan base-package="com.sage"/>
</beans>
- AOP 配置
- 1.添加命名空間
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop”
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="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/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
- 2.基於@AspectJ切面的驅動器配置
<aop:aspectj-autoproxy/>
如果使用的是CGLib動態代理,可以將其proxy-target-class屬性設置爲”true”,如:
<aop:aspectj-autoproxy proxy-target-class=”true”/>
- 3.最終配置
<?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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="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/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<aop:aspectj-autoproxy/>
</beans>
- 事務配置
- 1.添加命名空間
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="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/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
- 2. 使用@Transactional註解的配置
<!-- 加載Properties屬性文件 -->
<context:property-placeholder location="classpath*:/database.properties" ignore-resource-not-found="true" ignore-unresolvable="true" />
<!-- 配置一個數據源 -->
<bean id="proxoolDataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource">
<property name="driver">
<value>${driver_class}</value>
</property>
<property name="driverUrl">
<value>${driver_url}</value>
</property>
<property name="user" value="${username}"/>
<property name="password" value="${password}"/>
<!-- 與user,password相同,也可加入其他參數,這裏的user,password不能去掉,這可能是proxool的bug -->
<property name="delegateProperties">
<value>user=${username},password=${password}</value>
</property>
<!-- 允許最大連接數,超過了這個連接,再有請求時,就排在隊列中等候,最大的等待請求數由maximum-new-connections決定-->
<property name="maximumConnectionCount" value="50"/>
<!-- 最小連接數-->
<property name="minimumConnectionCount" value="5"/>
<!-- proxool自動偵察各個連接狀態的時間間隔(毫秒),偵察到空閒的連接就馬上回收,超時的銷燬-->
<property name="houseKeepingSleepTime" value="120000"/>
<!-- 最少保持的空閒連接數-->
<property name="prototypeCount" value="3"/>
<!-- 調試時顯示的別名 -->
<property name="alias" value="${username}"></property>
<!-- 跟蹤調試 -->
<property name="trace" value="true"/>
<!-- proxool自動偵察各個連接狀態的SQL -->
<property name="houseKeepingTestSql">
<value>select 1 from dual</value>
</property>
</bean>
<!-- 配置基於數據源的事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTrancationManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 開啓@Transactional事務註解,對@Transactional註解的Bean進行加工處理,以織入事務管理切面 -->
<tx:annotation-driven transaction-manager="transactionManager" />
- 3.最終配置
<?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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="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/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<!-- 加載Properties屬性文件 -->
<context:property-placeholder location="classpath*:/database.properties" ignore-resource-not-found="true" ignore-unresolvable="true" />
<!-- 配置一個數據源 -->
<bean id="proxoolDataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource">
<property name="driver">
<value>${driver_class}</value>
</property>
<property name="driverUrl">
<value>${driver_url}</value>
</property>
<property name="user" value="${username}"/>
<property name="password" value="${password}"/>
<!-- 與user,password相同,也可加入其他參數,這裏的user,password不能去掉,這可能是proxool的bug -->
<property name="delegateProperties">
<value>user=${username},password=${password}</value>
</property>
<!-- 允許最大連接數,超過了這個連接,再有請求時,就排在隊列中等候,最大的等待請求數由maximum-new-connections決定-->
<property name="maximumConnectionCount" value="50"/>
<!-- 最小連接數-->
<property name="minimumConnectionCount" value="5"/>
<!-- proxool自動偵察各個連接狀態的時間間隔(毫秒),偵察到空閒的連接就馬上回收,超時的銷燬-->
<property name="houseKeepingSleepTime" value="120000"/>
<!-- 最少保持的空閒連接數-->
<property name="prototypeCount" value="3"/>
<!-- 調試時顯示的別名 -->
<property name="alias" value="${username}"></property>
<!-- 跟蹤調試 -->
<property name="trace" value="true"/>
<!-- proxool自動偵察各個連接狀態的SQL -->
<property name="houseKeepingTestSql">
<value>select 1 from dual</value>
</property>
</bean>
<!-- 配置基於數據源的事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTrancationManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 掃描com.sage.service包 -->
<context:component-scan base-package="com.sage.service"/>
<!-- 開啓@Transactional事務註解,對@Transactional註解的Bean進行加工處理,以織入事務管理切面 -->
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>
- 三.Spring 註解
- @Component
使用@Component註解在類聲明處對類進行標註,它可以和在XML中配置達到同樣的效果。
package com.test;
import org.springframework.stereotype.Component;
@Component("a")
public class A {
...
}
等同於
<bean id="a" class="com.test.A" />
除了@Component外,Spring還提供了3個功能基本與之相同的註解,它們分別用於對DAO、Service以及Web層的Controller進行註解,所以也稱爲註解了Bean的衍型stereotype註解:
- @Repository:用於對DAO實現類進行標註;
- @Service:用於對Service實現類進行標註;
- @Controller:用於對Controller實現類進行標註。
之所以要在@Component之外提供這三個特殊的註解,是爲了讓標註類本身的用途清晰化,你完全可以用@Component替代這三個特殊的註解。但是推薦使用特定的註解標註特定的Bean。Spring在後續版本中可能會分別對這三個特殊的註解功能進行增強。
- @Repository
用於對DAO實現類進行標註;
- @Service
用於對Service實現類進行標註;
- @Controller
用於對Web層的Controller實現類進行標註;
- @AutoWired
使用@autoWired註解可以實現Bean的依賴注入。
該註解默認按類型匹配的方法,在容器中查找匹配的Bean,當有且僅有一個匹配的Bean時,Spring將其注入到@AutoWired標註的變量中。
如果窗口中沒有一個和標註變量類型匹配的Bean,Spring容器啓動時將報NoSuchBeanDefinitionException異常。如果希望Srping即使找不到匹配的Bean完成注入也不要拋出異常,那麼可以使用它的required屬性:@AutoWired(required=false),默認情況下,required屬性爲true。
package com.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component("a")
public class A {
@Autowired(required=false)
private B b;
...
}
對類方法進行標註:
@Autowired可以對類成品變量及方法的入庫進行標註,如:
package com.test;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class A {
private Bar bar;
private Car car;
@Autowired
public void setBar(Bar bar){
this.bar = bar;
}
@Autowired
public void int(Bar bar, Car car){
this.bar = bar;
this.car = car;
}
}
對集合類進行標註:
如果對類中集合類的變量或方法入參進行@Autowired標註,Spring會將容器中類型匹配的所有Bean都自動注入進來。如:
package com.test;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class T {
@Autowired
public List<Plugin> plugins;
public List<Plugin> getPlugins(){
return plugins;
}
}
Spring如果發現變量是一個集合類,它就會將容器中匹配集合元素類型的所有Bean都注入進來。這裏,Plugin爲一個接口,它擁有兩個實現類,分別是APlugin和BPlugin,這兩個實現類都通過@Component標註爲Bean,則Spring會將這兩個Bean都注入到plugins中。
- @Qualifier
如果容器中有一個以上匹配的Bean時,則可以和將@Autowired註解和@Qualifier註解一起使用:
package com.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component("a")
public class A {
@Autowired(required=false)
@Qualifier("b")
private B b;
...
}
這時,假設容器有兩個類型爲com.test.B的Bean,一個名爲”b”,另一個名稱”bb”,Spring會注入名爲”b”的Bean。
進類方法進行標註:
package com.test;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component
public class A {
private Bar bar;
private Car car;
@Autowired
@Qualifier("bar")
public void setBar(Bar bar){
this.bar = bar;
}
@Autowired
public void int(@Qualifier("bar")Bar bar, Car car){
this.bar = bar;
this.car = car;
}
}
- @Resource 和 @Inject
Spring還支持JSR-250中定義的@Resource和JSR-330中定義的@Inject註解,這兩個註解和@Autowired註解功能類似,都是對類變量及方法入參提供自動注入。
@Resource要求提供一個Bean名稱的屬性,如果爲空,則自動採用標註處的變量名或方法名作爲Bean的名稱:
package com.test;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.stereotype.Component;
@Component
public class A {
private Bar bar;
@Resource("bar")
public void setBar(Bar bar){
this.bar = bar;
}
}
@Inject註解和@autowired一樣也是按類型匹配注入Bean的,只不過它沒有required屬性。
可見不管是@resource還是@Inject註解,其功能都沒有@Autowired豐富,因此除非必要,大可不必在乎這兩個註解。
- @Scope
通過入參指定Bean的作用範圍,默認爲單例(singletion)。可選擇的值有:
- singletion
單據模式。每次獲取(注入)都是同一個實例。
- prototype
原型模式。每一次獲取(注入)都是新的實例。
- request (web應用環境相關的作用域 )
Request作用域的Bean對應一個Http請求和生命週期,這樣,每次Http請求調用到Bean時,Spring容器創建一個新的Bean實例,請求處理完畢後,銷燬這個實例。
- session (web應用環境相關的作用域 )
Session作用域的Bean對應一個Session請求和生命週期。Bean實例的作用域橫跨整個Http Session,Session中所有Http請求都共享同一個Bean實例。當Http Session結束後,實例才被銷燬。
- globalSession (web應用環境相關的作用域 )
書中所寫:globalSession作用域類似於Session作用域,不過僅在Portlet的Web應用中使用。Portlet規範定義了全局Session的概念,它被組成portlet Web應用的所有子Portlet共享。如果不在Portlet Web應用環境下,globalSession自然就等價於session作用域了。
使用Web應用環境相關的Bean作用域
需要在Web容器中進行額外配置:
在高版本的Web窗口中使用Http請求監聽器進行配置:
<listener>
listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
在低版本的Web容器中(before Servlet2.3)使用Http請求過濾器進行配置:
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- @PostConstruct 和 @PreDestory
@PostConsturct註解是用於標註初始化方法的,相當於xml配置中的init-method屬性;@PreDestory註解是用於標註銷燬方法的,相當於xml配置中的destory-method屬性。
package com.test;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import org.springframework.stereotype.Component;
@Component
public class A {
private Bar bar;
public A(){
System.out.println("construct...");
}
@Resource("bar")
public void setBar(Bar bar){
System.out.println("execute in setBar");
this.bar = bar;
}
@PostConstruct
private void init1(){
System.out.println("execute in init1");
}
@PostConstruct
public void init2(){
System.out.println("execute in init2");
}
@PreDestroy
public void destory1(){
System.out.println("execute in destory1");
}
@PreDestroy
public void destory2(){
System.out.println("execute in destory2");
}
}
運行這個類,將可以在控制檯中看到如下輸出信息:
construct...
execute in setBar
execute in init1
execute in init2
execute in destory1
execute in destory2
說明Spring先調用com.test.A的構造函數實例化Bean,再執行@Autowired進行自動注入,然後分別執行標註了@PostContruct的方法,容器關閉時,則分別執行標註了@PreDestory的方法。
- @DependsOn
一般情況下,可以使用@Autowired註解建立對其他Bean的依賴關係,Spring負責管理這些Bean的關係,當實例化一個Bean時,Spring保存該Bean所依賴的其他Bean已經初始化。
但在某些情況下,這種Bean之間的依賴關係並不那麼明顯。如某個論壇有很多系統參數(如會話過期時間、緩存更新時間),這些參數用於控制系統的運行邏輯。我們使用SystemSetting類表示這些系統參數:
public class SystemSetting{
public static int SESSION_TIMEOUT = 30;
public static int REFRESH_CYCLE = 60;
...
}
在SystemSetting中爲每一個系統參數提供了默認值,但一個靈活的論壇必須提供一個管理後臺,在管理後臺中可以調整這些系統參數並保存到後臺數據庫中,在系統啓動時,初始化程序從數據庫後臺加載這些系統參數的配置值以覆蓋默認的值:
@Component("sysInit")
public class SysInit{
public SysInit(){
SystemSetting.SESSION_TIMEOUT = 10;
SystemSetting.REFRESH_CYCLE = 100;
}
}
假設論壇有一個緩存刷新管理器,它需要根據系統參數SystemSettings.REFERSH_CYCLE創建緩存刷新定時任務:
@Component("cacheManager")
public class CacheManager{
public CacheManager(){
Timer timer = new Timer();
TimerTask task = new CacheTask();
timer.schedule(task, 0, SystemSetting.REFRESH_CYCLE);
}
}
在以上的實例中,CacheManager依賴於SystemSettings,而SystemSettings的值由SysInit負責初始化,雖然CacheManager不直接依賴於SysInit,但從邏輯上看,CacheManager希望在SysInit加載並完成系統參數設置後再啓動,以避免調用不到真實的系統參數值。如果這三個Bean都在Spring中定義,我們如何保存SysInit在CacheManager之前進行初始化呢?
Xml配置中,Spring允許用戶通過depends-on屬性指定Bean前置依賴的Bean,前置依賴的Bean會在本Bean實例化之前創建好。
而在使用註解的配置中,我們使用@DependsOn註解來標註前置依賴,多個前置依賴可以使用數組入參:
@DependsOn({"cacheManager"})
@Component("cacheManager")
public class CacheManager{
public CacheManager(){
Timer timer = new Timer();
TimerTask task = new CacheTask();
timer.schedule(task, 0, SystemSetting.REFRESH_CYCLE);
}
}
- @Lazy
通過@Lazy(true)註解表示Bean是否延遲初始化。
- @Configuration、@Bean、@Import、@ImportResource
Spring的JavaConfig子項目中的類,JavaConfig旨在通過Java害的方式提供Bean的定義信息,該項目早在Spring2.0時就已經發布了1.0版本。Srping3.0基於Java類配置的核心即取材於JavaConfig,JavaConfig經過若干年的努力終於修成正果,成爲了Spring3.0的核心功能。
@Configuration、@Bean、@Import、@ImportResource這四個註解都是用於該功能的。普通的POJO類只要標註了@Configruration註解,就可以爲Spring容器提供Bean定義的信息了。
- 四.Spring AOP
- 1.在application.xml中進行相關配置使用@AspectJ切面,請點擊查看 AOP配置。
- 2.@AspectJ語法基礎
Spring支持9個@ApsectJ切點表達式函數,它們用不同的方式描述目標類的連接點,根據描述對象的不同,可以大致分爲4種類型:
類別 |
函數 |
入參 |
說明 |
方法切點函數 |
execution() |
方法匹配模式串 |
表示滿足某一匹配模式的所有目標類方法連接點。語法: execution(<修飾符模式>?<返回類型模式><方法名模式><參數模式><異常模式>>) 每個模式之間用空格(“ ”)分開,如execution(public * *(..))、execution(* com.sage.Waiter.*(..))、execution(* com.sage..*(..))其中修飾符模式、異常模式是可以選的。這是最常用的切點函數。 支持所有的通配符。 |
@annotation() |
方法註解類名 |
表示標註了特定註解 的目標方法連接點。如@annotation(com.sage.anno.NeedTest)表示匹配任何標註了@NeedTest註解的目標類方法。 不支持任何通配符。 |
|
方法入參切點函數 |
args() |
類名、變量名 |
通過判別目標類方法運行時入參對象的類型指定連接點。如args(com.sage.A)表示所有有且僅有一個按類型匹配於A的入參方法。 僅支持通配符”+”,且默認實現”+”通配符,所以args(com.sage.A)等價於args(com.sage.A+)。 |
@args() |
類型註解類名 |
通過判別目標方法運行時入參對象的類是否標註特定註解來指定連接點。如@args(com.sage.Monitorable)表示任何這樣的一個目標方法:它有一個入參且入參對象的類標註@Monitorable註解。 不支持任何通配符。 |
|
目標類切點函數 |
within() |
類名匹配串 |
表示特定域下的所有連接點。如within(com.sage.service.*)表示com.sage.service包中所有連接點,即包中所有類的所有方法,而within(com.sage.service.*Service)表示匹配com.sage.service包中所有以Service結尾的類的所有連接點。 支持所有的通配符。 |
target |
類名 |
假如目標類按類型匹配於指定類,則目標類的所有連接點匹配這個切點。如通過target(com.sage.Waiter)定義的切點,Waiter以及Waiter的實現類及子類中的所有連接點都匹配該切點。 僅支持通配符”+”,且默認實現”+”通配符,所以target(com.sage.A)等價於target(com.sage.A+)。 |
|
@within() |
類型註解類名 |
假如目標類按類型匹配於某個類A,且類A標註了特定註解,則目標類的所有連接點匹配這個切點,如@within(com.sage.Monitorable)定義的切點,假如Waiter類標註了@Monitorable註解,則Waiter以及Waiter的實現類或子類的所有連接點都匹配。 不支持任何通配符。 |
|
@target() |
類型注o解類名 |
目標類標註了特定註解,則目標類所有連接點匹配該切點。如@target(com.sage.Monitorable),假如Waiter類標註了@Monitorable註解,那麼只匹配Waiter類,Waiter實現類或其子類並不匹配。 不支持任何通配符。 |
|
代理類切點函數 |
this() |
類名 |
代理類按類型匹配於指定類,則被代理的目標類所有連接點匹配切點。在一般情況下,使用this()和target()兩者是等效的;它們的不同不處,我們使用例子來說明:com.sage.NaiveWaiter實現Waiter接口,又通過引介增強引入com.sage.Seller接口中的方法,而this(com.sage.Seller)支持通過引介增強產生的NaiveWaiter代理對象,而target(com.sage.Seller)是不支持的。 僅支持通配符”+”,且默認實現”+”通配符,所以this(com.sage.A)等價於this(com.sage.A+)。 |
在函數入參中使用通配符
- *
匹配任意字符,但它只能匹配上下文中的一個元素;如com.sage.service.*Service,表示com.sage.service包中所有以Service結尾的類。
- ..
匹配任意字符,可以匹配上下文中的多個元素,但在表示類時,必須和*聯合使用,而在表示入參時則單獨使用;如com.sage..*,表示com.sage.service包及其子孫包中所有的類。
- +
表示按類型匹配指定類的所有類(類本身、實現類及子類),必須跟在類名後面,如com.sage.Car+,表示com.sage.Car類本身,以及com.sageCar的實現類和子類。
- 支持所有通配符的有:execution()、within()。
- 僅支持+通配符的有:args()、this()、target()。
- 不支持通配符的有:@args()、@within()、@target、@annotation()
邏輯運算符
- &&
與操作符,相當於切點的次運算;如within(com.sage.*) && args(java.lang.String) 表示com.sage包下(不包含其子孫包)的所有類擁有一個java.lang.String類型入參的方法。
如果在Spring的XML配置文件中使用切點表達款,由於&是XML特殊字符,所以需要使用轉義字符&&表示。爲了使用方便,Spring提供了一個等效的運算符”and”。如within(com.sage.*) and args(java.lang.String) 。
- ||
或操作符,相當於切點的並集運算。如within(com.sage.*) || args(java.lang.String),表示com.sage.包下所有類的方法,或者所有擁有一個java.lang.String類型入參的方法。
within(com.sage.*) or args(java.lang.String)與上述表達式一樣。
- !
非操作符,相當於切點的反集運算,如!within(com.sage.*)表示所有不在com.sage包下的方法。
“ not within(com.sage.*)”與述表達式一樣,注意:如果表達式是”not within(com.sage.*)”將產生解析錯誤,這應該是Spring的一個Bug,在表達式開頭添加空格後則可以通過:” not within(com.sage.*)”。
- and 同 && 一樣。
- or 同 || 一樣
- not 同 ! 一樣。
- 3.增強類型
- @Before
前置增強,在目標方法執行之前被調用。Before註解類擁有兩個屬性:
- value:該成員用於定義切點。
- argNames:由於無法通過Java反射機制獲取方法入參名,所以如果在Java編譯時未啓用高度信息或需要在運行期解析,就必須通過這個屬性指定註解所標註增強方法的參數名(注意兩者名字必須完全相同),多個參數名用逗號分隔。
- @AfterReturning
後置增強,在目標方法執行之後被調用。AfterReturning註解類擁有四個屬性:
- value:該屬性用於定義切點。
- pointcut:表示切點的信息,如果顯式指定pointcut值,它將覆蓋value的設置值,可以將pointcut屬性看成成是value的同義詞。
- returning:將目標對象方法的返回值綁定給增強的方法。
- argNames:如前所述。
- @Around
環繞增強,在目標方法執行之前被調用,需要在增加方法中通過ProceedingJoinPoint.proceed()方法執行目標方法。Around註解類擁有兩個屬性:
- value:該屬性用於定義切點。
- argNames:如前所述。
- @AfterThrowing
拋出增加,在目標方法拋出指定的異常時被調用。AfterThrowing註解類擁有四個屬性:
- value:該屬性用於定義切點。
- pointcut:表示切點的信息,如果顯式指定pointcut值,它將覆蓋value的設置值,可以將pointcut屬性看成成是value的同義詞。
- throwing:將拋出的異常綁定到增強方法中。
- argNames:如前所述。
- @After
Final增強,不管是拋出異常或是正常退出,該增強都會得到執行,該增強一般用於釋放資源,相當於try{}finally{}控制流。After註解類擁有兩個屬性:
- value:該屬性用於定義切點。
- argNames:如前所述。
- @DeclareParents
引介增強。它不是在目標方法周圍織入增強,而是爲目標類創建新的方法和屬性,所以引介增強的連接點是類級別的,而非方法級別的。通過引介增強可以爲目標類添加一個接口的實現,即原來目標類未實現某個接口,通過引介增強可以爲目標類創建實現某接口的代理。該增強有點難理解,下面會通過一個實例來說明。DeclareParents擁有兩個屬性:
- value:該屬性用於定義切點,它表示在哪個目標類上添加引介增強。
- defaultImpl:默認的接口實現類。
請看以下兩個接口及其實現類:
假設我們希望NaiveWaiter能夠同時充當售貨員的角色,即通過切面技術爲NaiveWaiter新增Seller接口的實現。實現代碼如下:
package com.test;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
@Aspect
public class EnableSellerAspect {
@DeclareParents(value="com.sage.NaiveWaiter", defaultImpl=SmartSeller.class)
public Seller seller;
}
運行以下測試代碼:
package com.test;
import org.springframework.context.ApplicationContext;
public class EnableSellerAspectTest {
public static void main(String[] args) {
...
ApplicationContext ctx = ...;
Waiter waiter = (Waiter)ctx.getBean("waiter");
System.out.println(waiter instanceof Waiter);
Seller seller = (Seller)waiter;
System.out.println(seller instanceof Seller);
}
}
代碼成功執行,並輸出以下信息:
true
true
可見,NaiveWaiter已經成功地新增了Seller接口的實現。
- 4.@args()、@within()、@target()詳細介紹
- @args()
該函數接受一個註解類的類名,當方法的運行時入參對象標註了指定的註解時,方法匹配切點。如圖:
T0、T1、T2、T3具有如圖所示的繼承關係,假設目標類方法的簽名爲fun(T1 t),它的入參爲T1,而切面的切點定義爲@args(M),T2類標註了@M。當fun(T1 t)傳入對象是T2或T3時,則方法匹配@args(M)所聲明定義的切點。
再看下面的情況,假設方法簽名是fun(T1 t),入參爲T1,而標註@M註解的類是T0,當fun(T1 t)傳入T1、T2、T3的實例時,均不匹配切點@args(M),如圖:
- @target()
該函數匹配任意標註了@M的目標類,不包含其實現類和子類。
如@target(M)切點的匹配規則如圖:
- @within()
該函數匹配任意標註了@M的目標類及其子孫類。
如@within(M)切點的匹配規則如圖:
- 5.參數綁定
爲了方便講解,我們假設可供被增強的目標類包括7個類,這些類都在com.sage.*包中,如圖:
- 綁定連接點方法入參
args()、this()、target()、@args()、@within()、@target()、@annotation()這七個函數除了可以指定類名外,還可以指定參數名,將瞟對象連接點的方法入參綁定到增強的方法中。
其中args()用於綁定連接點方法的入參;@annotation()用於綁定連接點方法的註解對象;@args()用於綁定連接點方法入參的註解。來看一個args()綁定參數的實例:
package com.test;
import org.aspectj.lang.annotation.Before;
public class TestAspect {
@Before("target(com.sage.NaiveWaiter)&&args(name,num,..)")
public void bindJoinPointParams(int num, String name){
System.out.println("---bindJoinPointParams()---");
System.out.println("name:"+name);
System.out.println("num:"+num);
System.out.println("---bindJoinPointParams()---");
System.out.println("以下是目標方法的輸出:");
}
}
當前args()函數入參爲參數名時,共包括兩方面的信息:
- 連接點匹配規則信息:連接點方法第一個入參是String類型,第二個入參是int類型;
- 連接點方法入參和增強方法入參的綁定信息:連接點方法的第一個入參綁定到增強方法的name參數上,第二個入參綁定到增強方法的num入參上。
運行下面的測試代碼:
ApplicationContext ctx = new ...;
NaiveWaiter naiveWaiter = ctx.getBean("naiveWaiter");
naiveWaiter.smile("John", 2);
System.out.println("結束!");
將在控制檯看到以下輸出信息:
---bindJoinPointParams()---
name:John
num:2
---bindJoinPointParams()---
以下是目標方法的輸出:
NaiverWaiter:smile to Johe 2 times.
結束!
可見增強方法按預期綁定了NaiveWaiter.smile(String name, int times)方法的運行期入參.
爲了保存實例能成功執行,必須啓用CGLib動態代碼,在XML配置文件中如此配置:<aop:aspectj-autoproxy proxy-target-class=”true”/>。
- 綁定代理對象
使用this()或target()可綁定被代理對象實例,在通過類實例名綁定對象時,還依然具有原來連接點匹配的功能,只不過類名是通過增強方法中同名入參的類型間接決定的。
這裏通過this()函數來了解對象綁定的用法:
package com.test;
import org.aspectj.lang.annotation.Before;
public class TestAspect {
@Before("this(waiter)")
public void bindProxyObj(Waiter waiter){
System.out.println("---bindProxyObj()---");
System.out.println(waiter.getClass().getName());
System.out.println("---bindProxyObj()---");
System.out.println("以下是目標方法的輸出:");
}
}
運行下面的測試代碼:
ApplicationContext ctx = new ...;
Waiter naiveWaiter = ctx.getBean("naiveWaiter");
naiveWaiter.greetTo("John");
System.out.println("結束!");
將在控制檯看到以下輸出信息:
---bindProxyObj()---
com.sage.NaiveWaiter$$EnhancerByCGLIB$$6758801b
---bindProxyObj()---
以下是目標方法的輸出:
NaiverWaiter:greet to John.
結束!
target()函數的代理對象綁定跟上面一樣。
- 綁定類註解對象
@within()和@target()函數可以將目標類的註解對象綁定到增強方法中,下面通過@within()函數演示註解綁定的操作:
package com.test;
import org.aspectj.lang.annotation.Before;
public class TestAspect {
@Before("@within(m)")
public void bindAnnoObj(Monitorable m){
System.out.println("---bindAnnoObj()---");
System.out.println(m.getClass().getName());
System.out.println("---bindAnnoObj()---");
System.out.println("以下是目標方法的輸出:");
}
}
NaiveWaiter類中標註了@Monitorable註解,所有NaiveWaiter Bean匹配切點,其Monitorable註解對象將綁定到增強方法中。運行下面的測試代碼:
ApplicationContext ctx = new ...;
Waiter naiveWaiter = ctx.getBean("naiveWaiter");
naiveWaiter.greetTo("John");
System.out.println("結束!");
將在控制檯看到以下輸出信息:
---bindAnnoObj()---
$Proxy3
---bindAnnoObj()---
以下是目標方法的輸出:
NaiverWaiter:greet to John.
結束!
從輸出信息中可以看出,使用CGLib代理NaiveWaiter時,其類的註解Monitorable對象也被代理了。
- 綁定返回值
在後置增強中,可以通過returning綁定連接點方法的返回值:
@AfterReturning(value="target(com.sage.SmartSeller)", returning="retVal")
public void bindReturnValue(int retVal){
System.out.println("---bindReturnValue()---");
System.out.println("return value:" + retVal);
System.out.println("---bindReturnValue()---");
System.out.println("以下是目標方法的輸出:");
}
Returning屬性值的名稱必須跟增強方法入參的名稱相同,此外,returning屬性值的類型必須和連接點方法的返回值類型匹配。運行下面的測試代碼:
ApplicationContext ctx = new ...;
SmartSeller seller = ctx.getBean("seller");
seller.sell("John", "beer");
System.out.println("結束!");
可以看到以下輸出信息:
---bindReturnValue()---
return value:100
---bindReturnValue()---
以下是目標方法的輸出:
SmartSeller:Johe sell to beer.
結束!
- 綁定拋出的異常
和通過切點函數綁定連接點信息不同,連接點拋出的必須使用@AfterThrowing註解的throwing屬性進行綁定:
@AfterThrowing(value="target(com.sage.SmartSeller)", throwing="e")
public void bindException(NullPointerException e){
System.out.println("---bindException()---");
System.out.println("exception:" + e.getMessage());
System.out.println("---bindException()---");
System.out.println("以下是目標方法的輸出:");
}
在SmartSeller中添加一個拋出異常的方法:
public class SmartSell implements Seller {
public void checkBill(String billId){
if(billId==null) throw new NullPointerException("billId is null.");
}
}
運行下面的測試代碼:
ApplicationContext ctx = new ...;
SmartSeller seller = ctx.getBean("seller");
seller.checkBill(null);
System.out.println("結束!");
可以看到以下輸出信息:
---bindException()---
exception:billId is null
---bindException()---
以下是目標方法的輸出:
SmartSeller:Johe sell to beer.
結束!
- 6.增強順序
一個連接點可以同時匹配多個切點,切點對應的增強在連接點上的織入順序是如何安排的呢?這個問題需要分3種情況討論:
- 如果增強在同一個切面類中聲明,則依照增強在切面類中定義的順序進行織入;
- 如果增強位於不同的切面類中,且這些切面類都實現了org.springframework.core.Ordered接口,則由接口方法的順序號決定(順序號小的先織入);
- 如果增強位於不同的切面類中,且這些切面類沒有實現org.springframework.core.Ordered接口,織入的順序是不確定的。
我們可以通過下圖描述這種織入的規則:
切面類A和B都實現了Ordered接口,A切面類對應序號爲1,B切面類對應序號爲2,A切面類按順序定義了3個增強,B切面類按順序定義兩個增強,這5個增強對應的切點都匹配某個目標類的連接點,則增強織入的順序爲上圖中虛線所示。
- 7.訪問連接點信息
AspectJ使用org.aspectj.lang.JoinPoint接口表示目標類連接點對象,如果是環線增強時,使用org.aspectj.lang.ProceedingJoinPoint表示連接點對象,該類是JoinPoint的子接口。任何一個增強方法都可以通過將第一個入參聲明爲JoinPoint來訪問到連接點上下文的信息。
- JoinPoint
- java.lang.Object[] getArgs() : 獲取連接點方法運行時入參列表;
- org.aspectj.lang.Signature getSignature() : 獲取連接點的方法簽名對象;
- java.lang.Object getTarget() : 獲取連接點所在的目標對象;
- java.lang.Object getThis() : 獲取代理對象本身。
- ProceedingJoinPoint
ProceedingJoinPoint繼承JoinPoint子接口,它新增了兩個用於執行連接點方法的方法:
- java.lang.Object proceed() throws java.lang.Throwable : 通過反射執行目標對象的連接點處的方法;
- java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable : 通過反射執行目標對象連接點處的方法,不過使用新的入參替換原來的入參。
看一個具體的實例:
package com.test;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
public class TestAspect {
@Around("execution(* geetTo(..)) && target(com.sage.NaiveWaiter)")
public void joinPointAccess(ProceedingJoinPoint p){
System.out.println("---joinPointAccess()---");
System.out.println("args[0]:" + p.getArgs()[0]);
System.out.println("target class:" + p.getTarget().getClass());
System.out.println("以下是目標方法的輸出:");
p.proceed();
System.out.println("---joinPointAccess()---");
}
}
執行以下測試代碼:
ApplicationContext ctx = new ...;
Waiter waiter = ctx.getBean("naiveWaiter");
waiter.greetTo("John");
System.out.println("結束!");
可以看到以下輸出信息:
---joinPointAccess()---
args[0]:John
target class:com.sage.NaiveWaiter
以下是目標方法的輸出:
NaiveWaiter:greet to John.
---joinPointAccess()---
結束!
- 8.命名切點
在前面所舉的例子中,切點直接聲明在楊增強處,這種切點聲明方式稱爲匿名切點,匿名切點只能在聲明處使用。如果希望在其他地方重用一個切點,可以通過@Pointcut註解以及切面類方法對切點進行命名,如下:
package com.test;
import org.aspectj.lang.annotation.Pointcut;
public class TestNamePointcut {
@Pointcut("within(com.sage.*)")
private void inPackage(){}
@Pointcut("execution(* greetTo(..))")
protected void greetTo(){}
@Pointcut("inPackage() && greetTo()")
public void inPkgGreetTo(){}
}
在上述代碼中定義了3個命名切點,命名切點的使用類方法作爲切點的名稱,此外方法的訪問修飾符還控制了切點的可引用性,這種可引用性和類方法的可訪問性相同,如private的切點只能在本類中引用,public的切點可以在任何類中引用。命名切公利用方法名和訪問修飾符的信息,所以習慣上,方法的返回類型爲void,並且方法體爲空。
通過下圖可以更直觀的瞭解命名切點的結構:
在上述代碼中inPkgGreetTo()的切點引用了同類中的greetTo()切點,而inPkgGreetTo()切點可以被任何類引用。我們還可以擴展TestNamePointcut類,通過類的繼承關係定義更多的切點。
命名切點定義好之後,就可以在定義切面時通過名稱引用切點,看下面的實例:
package com.test;
import org.aspectj.lang.annotation.Before;
public class TestAspect {
@Before("TestNamePointcut.inPkgGreetTo()")
public void pkgGreetTo(){
System.out.println("- pkgGreetTo() executed! -");
}
@Before("!target(com.sage.NaiveWaiter) && TestNamePointcut.inPkgGreetTo()")
public void pkgGreetToNotNaiveWaiter(){
System.out.println("- pkgGreetToNotNaiveWaiter() executed! -");
}
}
可以看出,命名切點可以使用複合運算和匿名切點一起使用。
- 五.Spring 事務管理
- 1.數據併發的問題
一個數據庫可能擁有多個訪問客戶端,這些客戶端都可以以併發方式訪問數據庫。數據庫中的相同數據可能同時被多個事務訪問,如果沒有采取必要的隔離措施,就會導致各種併發問題,破壞數據的完整性。這些問題可以歸結爲5類,包括3類數據讀問題(髒讀、不可重複讀、幻象讀)以及2類數據更新問題(第一類丟失更新、第二類丟失更新)。下面,我們分別通過實例講解引發問題的場景。
- 髒讀(dirty read)
A事務讀取B事務尚未提交的更改數據,並在這個數據的基礎上操作。如果恰巧B事務回滾,那麼A事務讀到的數據根本是不承認的。來看取款事務和轉賬事務併發時引發的髒讀場景:
時間 |
轉賬事務A |
取款事務B |
T1 |
|
開始事務 |
T2 |
開始事務 |
|
T3 |
|
查詢賬戶餘額爲1000元 |
T4 |
|
取出500元,把餘額改爲500元 |
T5 |
查詢賬戶餘額爲500元(髒讀) |
|
T6 |
|
撤銷事務,餘額恢復爲1000元 |
T7 |
匯入100元,把餘額改爲600元 |
|
T8 |
提交事務 |
|
在這個場景中,B希望取款500元而後又撤銷了動作,而A住相同的賬戶中轉賬100元,就因爲A事務讀取了B事務尚未提交的數據,因而造成賬戶白白丟失了500元。在Oracle數據庫中,不會發生髒讀的情況。
- 不可重複讀(unrepeatable read)
不可重複讀是批A事務讀取了B事務已經提交的更改數據。假設A在取款事務的過程中,B往該賬戶轉賬100元,A兩次讀取賬戶的餘額發生不一致:
時間 |
取款事務A |
轉賬事務B |
T1 |
|
開始事務 |
T2 |
開始事務 |
|
T3 |
|
查詢賬戶餘額爲1000元 |
T4 |
查詢賬戶餘額爲1000元 |
|
T5 |
|
取出100元,把餘額改爲900元 |
T6 |
|
提交事務 |
T7 |
查詢賬戶餘額爲900元(和T4讀取的不一致) |
|
在同一事務中,T4時間點和T7時間點讀取賬戶存款餘額不一樣。
- 幻象讀(phantom read)
A事務讀取B事務提交的新增數據,這時A事務將出現幻象讀的問題。幻象讀一般發生在計算統計數據的事務中,舉一個例子,假設銀行系統在同一個事務中,兩次統計存款賬戶的總金額,在兩次統計過程中,剛好新增了一個存款賬戶,並存入100元,這時,兩次統計的總金額將不一致:
時間 |
統計金額事務A |
轉賬事務B |
T1 |
|
開始事務 |
T2 |
開始事務 |
|
T3 |
統計總存款數爲10000元 |
|
T4 |
|
新增一個存款賬戶,存款爲100元 |
T5 |
|
提交事務 |
T6 |
再次統計總存款數爲10100元(幻象讀) |
|
如果新增數據剛好滿足事務的查詢條件,這個新數據就進入了事務的視野,因而產生了兩個統計不一致的情況。
幻象讀和不可重複讀是兩個容易混淆的概念,前者是指讀到了其他已經提交事務的新增數據,而後者是指讀到了已經提交事務的更改數據(更改或刪除),爲了避免這兩個情況,採取的對策是不同的,防止讀取到更改數據,只需要對操作的數據添加行級鎖,阻止操作中的數據發生變化,而防止讀取到新新增數據,則往往需要添加表級鎖 -- 將整個表鎖定,防止新增數據(Oracle使用多版本數據的方式實現)。
- 第一類丟失更新
A事務撤銷時,把已經提交的B事務的更新數據覆蓋了。這種錯誤可以造成很嚴重的問題,通過下面的賬戶取款轉賬就可以看出來:
時間 |
取款事務A |
轉賬事務B |
T1 |
開始事務 |
|
T2 |
|
開始事務 |
T3 |
查詢賬戶餘額爲1000元 |
|
T4 |
|
查詢賬戶餘額爲1000元 |
T5 |
|
匯入100元把餘額改爲1100元 |
T6 |
|
提交事務 |
T7 |
取出100元把餘額改爲900元 |
|
T8 |
撤銷事務 |
|
T9 |
餘額恢復爲100元(丟失更新) |
|
A事務在撤銷時,“不小心”將B事務已經轉入賬戶的金額給抹去了。
- 第二類丟失更新
A事務覆蓋B事務已經提交的數據,造成B事務所做操作丟失:
時間 |
轉賬事務A |
取款事務B |
T1 |
|
開始事務 |
T2 |
開始事務 |
|
T3 |
|
查詢賬戶餘額爲1000元 |
T4 |
查詢賬戶餘額爲1000元 |
|
T5 |
|
取出100元,把餘額改爲900元 |
T6 |
|
提交事務 |
T7 |
匯入100元 |
|
T8 |
提交事務 |
|
T9 |
把餘額改爲1100元(丟失更新) |
|
上面的例子裏由於轉賬事務A覆蓋了取款事務對存款餘額所做的更新,導致銀行最後損失了100元,相反如果轉賬事務先提交,那麼用戶賬戶將損失100元。
- 2.數據庫鎖機制
數據併發會引發很多問題,在一些場合下有些問題是允許的,但在另外一些場合下可能卻是致命的。數據庫通過 鎖的機制解決併發訪問的問題,雖然不同的數據庫在實現細節上存在差別,但原理基本上是一樣的。
按鎖定的對象的不同,一般可以分爲表鎖定和行鎖定,前者對整個表進行鎖定,而後者對錶中特定行進行鎖定。從併發事務鎖定的關係上看,可以分爲共享鎖定和獨佔鎖定。共享鎖定會防止獨佔鎖定,但允許其他的共享鎖定。而獨佔鎖定 既防止蒼的獨佔鎖定,也防止其他的共享鎖定。爲了更改數據,數據庫必須在進行更新的行上施加行獨佔鎖定,INSERT、UPDATE、DELETE和SELECT FOR UPDATE語句都會隱式採用必要的行鎖定。下面我們介紹一下Oracle數據庫常用的5種鎖定:
- 行共享鎖定:
一般通過SELECT FOR UPDATE語句隱式獲得行共享鎖定,在Oracle中用戶也可以通過LOCK TABLE IN ROW SHARE MODE語句顯式獲得行共享鎖定。行共享鎖定並不防止對數據行進行更新的操作,但是可以防止 其他會話獲取獨點性數據表鎖定。允許進行多個併發的行共享和行獨佔性鎖定,還允許進行數據表的共享鎖定或者表共享行獨佔鎖定。
- 行獨佔鎖定:
通過一條INSERT、UPDATE或DELETE語句隱式獲取,或者通過一條LOCK TABLE IN ROW EXCLUSIVE MODE 語句顯式獲取。這個鎖定可以防止其他會話獲取一個表共享鎖定、行共享鎖定、表共享行獨佔鎖定、行獨佔鎖定、表獨佔鎖定。
- 表共享鎖定:
通過LOCAK TABLE IN SHARE MODE語句顯式獲得。這種鎖定可以防止其他會話獲取行獨佔鎖定(INSERT、UPDATE、DELETE),或者防止其他表共享行獨佔鎖定或表獨佔鎖定,它允許在表中擁有多個行共享和表共享鎖定。該鎖定可以讓會話具有對錶事務級一致性訪問,因爲其他會話在用戶提交匱乏回溯該事務並釋放對該表的鎖定之前不能更改這個被鎖定的表。
- 表共享行獨佔:
通過LOCK TABLE IN SHARE ROW EXCLUSIVE MODE語句顯式獲得。這種鎖定可以防止其他會話獲取一個表共享、行獨佔或者表獨佔鎖定,這允許其他行共享鎖定。這種鎖定類似於表共享鎖定,只是一次只能對一個表旋轉一個表共享行獨佔鎖定。如果A會話擁有該鎖定,則B會話可以執行SELECT FOR UPDATE操作,如果B會話試圖更新選擇的行,則需要等待。
- 表獨佔
通過LOCK TABLE IN EXCLUSIVE MODE顯式獲得。這個鎖定防止其他會話對該表的任何其他鎖定。
- 3.事務隔離級別
儘管數據庫爲用戶提供了鎖的DML操作方式,但直接使用鎖管理是非常麻煩的,因此數據庫爲用戶提供了自動鎖機制。只要用戶指定會話的事務隔離級別,數據庫就會分析事務中的SQL語句,然後自動爲事務操作的數據資源添加上適合的鎖。些外數據庫還會維護這些鎖,當一個資源上的鎖數目太多時,自動進行鎖升級以提高系統的運行性能,而這一過程對用戶來說完全是透明的。
ANSI/ISO SQL 92標準定義了4個等級的事務隔離級別,在相同數據環境下,使用相同的輸入,執行相同的工作,根據不同的隔離級別,可以導致不同的結果。不同事務隔離級別能夠解決的數據併發問題的能力是不同的,如下所示:
隔離級別 |
髒讀 |
不可重複讀 |
幻象讀 |
第一類丟失更新 |
第二類丟失更新 |
READ UNCOMMITED |
允許 |
允許 |
允許 |
不允許 |
允許 |
READ COMMITED |
不允許 |
允許 |
允許 |
不允許 |
允許 |
REPEATABLE READ |
不允許 |
不允許 |
允許 |
不允許 |
不允許 |
SERIALIZABLE |
不允許 |
不允許 |
不允許 |
不允許 |
不允許 |
事務的隔離級別和數據庫併發性是對立的,兩者此增彼減。一般來說,使用READ UNCOMMITED隔離級別的數據庫擁有最高的併發性和吞吐量,而使用SERIALIZABLE隔離級別的數據庫併發性最低。
SQL 92定義READ UNCOMMITED主要是爲了提供非阻塞坊的能力,Oracle雖然也支持READ UNCOMMITED,但它不支持髒讀,因爲Oracle使用多版本機制徹底解決了在非阻塞讀到髒數據的問題並保證讀的一致性,所以Oracle的READ COMMITED隔離級別就已經滿足了SQL 92標準的REFEATABLE READ隔離級別。
SQL 92推薦使用REPEATABLE READ以保證數據的讀一致性,不過用戶可以根據就用的需要選擇適合的隔離等級。
- 4.Spring的事務管理器實現類
通過JDBC的事務管理知識,我們知道事務只能被提交或回滾(或回滾到某個保存點後提交),Spring高層事務抽象接口org.springframework.transaction.PlatformTransactionManager很好的描述了事務管理的這個概念,以下是其代碼:
public interface PlatformTransactionManager {
TransactionStratus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStratus status) throws TransactionException;
void rollback(TransactionStratus status) throws TransactionException;
}
PlatformTransactionManager只定義了3個接口方法,它們是SPI(Service Provider Interface)高層次的接口方法。這些訪問都沒有和JNDI綁定在一起,可以像Spring容器中普通的Bean一樣對待PlatformTransactionManager實現者。下面我們來了解PlatformTransactionManager方法的功能:
- TransactionStratus getTransaction(TransactionDefinition definition)
該方法根據事務定義信息從事務環境中返回一個已存在的事務,或者創建一個新的事務,並用TransactionStratus描述這個事務的狀態。
- void commit(TransactionStratus status)
根據事務的狀態提交事務,如果事務狀態已經被標識爲rollback-only,該方法將執行一個回滾事務的操作。
- void rollback(TransactionStratus status)
將事務回滾。當commit()方法拋出異常時,rollback()會被隱式調用。
Spring將事務管理委託給底層具體的持久化實現框架完成。因此,Spring爲不同的持久化框架提供了PlatformTransactionManager接口的實現類,如下所示:
事務 |
說明 |
org.springframework.orm.jpa.JpaTransactionManager |
使用JPA進行持久化時,使用該事務管理器 |
Org.springframework.orm.hibernate3.HibernateTransactionManager |
使用Hibernate3.0版本進行持久化,使用該事務管理器 |
org.springframework.jdbc.datasource.DataSourceTransactionManager |
使用Spring JDBC或iBatis等基於DataSource數據源的持久化技術時,使用該事務管理器 |
org.springframework.orm.jdo.JdoTransactionManager |
使用JDO進行持久化時,使用該事務管理器 |
org.springframework.transaction.jta.JtaTransactionManager |
具有多個數據源的全局事務使用該事務管理器(不管理採用何種持久化技術) |
這些事務管理器都是對特定事務實現框架的代理,這樣,我們就可以通過Spring所提交的高級抽象對不種類的事務實現使用相同的方式進行管理,而不用關心具體的實現。
要實現事務,首先要在Spring中配置好相應的事務管理器,爲事務管理器指定數據資源以及一些其他事務管理控制屬性。下面讓我們看一個JDBC和iBatis的事務管理器配置:
...
<bean id="dataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource"
p:driver="${driver_class}"
p:driverUrl="${driver_url}"
p:user="${username}"
p:password="${password}"
p:delegateProperties="user=${username},password=${password}"
/>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource" />
...
在幕後,DataSourceTransactionManager使用DataSource的Connection的commit()、rollback()等方法管理事務。
- 5.Spring的事務同步管理器
Spring將JDBC的Connection、Hibernate的Session等訪問數據庫的連接或會話對象統稱爲資源。這些資源在同一時刻是不能多線程共享的,爲了讓Dao、Service類可能做到Singleton,Spring的事務同步管理器類org.springframework.transaction.support.TransactionSynchronizationManager使用ThreadLocal管理爲不同事務線程提供了獨立的資源副本,同時維護事務配置的屬性和運行狀態信息。事務同步管理器是Spring事務管理的基石部分,不管用戶使用編程式事務管理,還是聲明式事務管理,都離不開事務同步管理器。
Spring框架爲不同的持久化技術提供了一套從TransactionSynchronizationManager中獲取對應線程綁定資源的工具類,如下所示:
待久化技術 |
線程綁定資源獲取工具 |
Spring JDBC或iBatis |
org.springframework.jdbc.datasource.DataSourceUtils |
Hibernate 3.0 |
org.springframework.orm.Hibernate3.SessionFactoryUtils |
JPA |
org.springframework.jpa.EntityManagerFactoryUtils |
JDO |
org.springframework.orm.jdo.PersistenceManagerFactoryUtils |
這些工具類都提供了靜態的方法,通過這些當前線程綁定的資源,如果DataSourceUtils.getConnection(DataSource dataSource)可以從數據源中獲取和當前線程綁定的Connection,而Hibernate的SessionFactoryUtils.getSession(SessionFactory sessionFactory, boolean allowCreate)則從指定的SessionFactory中獲取和當前線程綁定的Session。
當需要脫離模版類,手工操作底層持久技術的原生API時,就需要通過這些工具類獲取線程綁定的資源,而不應該直接從DataSource或SessionFactory中獲取。因爲後者不能獲得和本線程相關的資源,因此無法讓數據操作參與到本線程相關的事務環境中。
這些工具類還有另外一個重要的用途:將特定異常轉換爲Spring的DAO異常。
Spring爲不同的持久化技術提供了模版類,模板類在內部通過資源獲取工具類間接訪問TransactionSynchronizationManager中的線程綁定資源。所以如果Dao使用模版類進行持久化操作,這些Dao就可以配置成singleton。如果不使用模板類,也可以直接通過資源獲取工具類訪問線程相關的資源。
下面,我們就揭開TransactionSynchronizationManager的層層面紗,探尋其中的奧祕:
public abstract class TransactionSynchronizationManager{
//①②③④⑤用於保存每個事務線程對應的Connection或Session等類型的資源
private static final ThreadLocal resources = new ThreadLocal();
//②用於保存每個事務線程對應事務的名稱
private static final ThreadLocal currentTransactionName = new ThreadLocal();
//③用於保存每個事務線程對應事務的read-only狀態
private static final ThreadLocal currentTransactionReadOnly = new ThreadLocal();
//④用於保存每個事務線程對應事務的隔離級別
private static final ThreadLocal currentTransactionIsolationLevel = new ThreadLocal();
//⑤用於保存每個事務線程對應事務的激活態
private static final ThreadLocal actualTransactionActive = new ThreadLocal();
...
}
TransactionSynchronizationManager將Dao、Service類中影響線程安全的所有“狀態”統一抽取到該類中,並用ThreadLocal進行替換,從此Dao(必須基於模板類或資源獲取工具類創建的Dao)和Service(必須採用Spring事務管理機制)摘掉了非線程安全的帽子,完成了脫胎換骨式的身份轉變。
- 6.Spring的事務傳播行爲
當我們調用一個基於Spring的Service接口方法(如UserService#addUser())時,它將運行於Spring管理的事務環境中,Service接口方法可能會在內部調用其他的Service接口方法以共同完成一個完整的業務操作,因此就會產生服務接口方法嵌套調用的情況,Spring通過事務傳播行爲控制當前的事務如何傳播到被嵌套調用的目標服務接口方法中。事務傳播是Spring進行事務管理的重要概念,其重要性怎麼強調都不爲過。但是事務傳播行爲也是被誤解最多的地方,接下來我們將詳細分析不同事務傳播行爲的表現形式,掌握它們之間的區別。
Spring在TransactionDefinition接口中規定了7種類型的事務傳播行爲,它們規定了事務方法和事務方法發生嵌套調用時事務如何進行傳播,如下所示:
事務傳播行爲類型 |
說明 |
PROPAGATION_REQUIRED |
如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。 |
PROPAGATION_SUPPORTS |
支持當前事務,如果當前沒有事務,就以非事務方式執行。 |
PROPAGATION_MANDATORY |
使用當前的事務,如果當前沒有事務,就拋出異常。 |
PROPAGATION_REQUIRES_NEW |
新建事務,如果當前存在事務,把當前事務掛起。 |
PROPAGATION_NOT_SUPPORTED |
以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。 |
PROPAGATION_NEVER |
以非事務方式執行,如果當前存在事務,則拋出異常。 |
PROPAGATION_NESTED |
如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作 -- 新建一個事務。 |
(關於PROPAGATION_NESTED和PROPAGATION_REQUIRES_NEW區別可以點擊http://www.07net01.com/linux/spring_PROPAGATION_REQUIRES_NEW_he_PROPAGATION_NESTED_499731_1373007731.html查看)
- 7.使用註解配置聲明式事務
- XML配置
具體配置可以看事務配置。
- @Transactional的屬性
基於@Transactional註解的配置和基於XML的配置方式一樣,它擁有一組普適性很強的默認事務屬性,我們往往可以直接使用這些默認的屬性就可以了:
- 事務傳播行爲:PROPAGATION_REQUIED;
- 事務隔離級別:ISOLATION_DEFAULT;使用數據庫默認的事務隔離級別。(Oracle默認的隔離級別是READ COMMITED,Mysql默認的隔離級別是REPEATABLE READ)。
- 讀寫事務屬性:讀/寫事務;
- 超時時間:依賴於底層的事務系統的默認值;
- 回滾設置:任何運行期異常引發回滾,任何檢查型異常不會引發回滾。
因爲這些默認設置在大多數情況下都是適用的,一般不需要手工設置事務註解的屬性。當前,Spring允許我們通過手工設定屬性值覆蓋默認值。下面就是@Transactional的屬性說明:
屬性名 |
說明 |
propagation |
事務傳播行爲,通過以下枚舉類提供合法值: ora.springframework.transaction.annotation.Propagation 例如:@Transactional(propagation=Propagation.REQUIRES_NEW) |
isolation |
事務隔離級別,通過以下枚舉類提供合法值: org.springframework.transaction.annotation.Isolation 例如:@Transactional(isolation=Isolation.READ_COMMITTED) |
readOnly |
事務讀寫性,boolean型,例如:@Transactional(readOnly=true) |
timeout |
超時時間,int型,以秒爲單位,例如:@Transactional(timeout=10) |
rollbackFor |
一組異常類,遇到時進行回滾,類型爲:Class<? extends Throwable>[],默認爲{}。 例如:@Transactional(rollbackFor={SQLException.class}),多個異常之間可用逗號分隔。 |
rollbackForClassName |
一組異常類名,遇到時進行回滾,類型爲String[],默認值爲{}。例如: @Transactional(rollbackForClassName={“Exception”}) |
noRollbackFor |
一組異常類,遇到時不回滾,類型爲:Class<? extends Throwable>[],默認爲 |
noRollbackForCassName |
一組異常類名,遇到時不回滾,類型爲String[],默認值爲{}。 |
- @Transactional如何使用
在何必使用@Transactional註解
@Transactional註解可以被應用於接口定義和接口方法、類定義、和類的public方法上。
但Spring建議在業務實現類上使用@Transactional註解,當然我們也可以在業務接口上使用@Transactional註解。但這樣會留下一容易被忽視的隱患。因爲註解不能被繼承,所以業務接口中標註的@Transactional註解不會被實現的業務類繼承,如果通過以下的配置啓用子類代理:
<tx:annotation-driven transaction-manager=”txManager” proxy-target-class=”true” />
業務類不會添加事務增強,照樣工作在非事務的環境下。舉一個具體的實例:如果使用子類代理,假設用戶爲BbtForum接口標註了@Transactional註解,其實現類BbtForumImpl依舊不會啓用事務機制。
因此,Spring建議在具體業務類上使用@Transactional註解。這樣不管理<tx:annotation-driven>將proxy-target-class屬性配置爲true或false,業務類都會啓用事務機制。
在方法處使用註解
方法處的註解會覆蓋類定義處的註解,如果有些方法需要使用特殊的事務屬性,則可以在類註解的基礎上,提供方法註解:
//①類級的註解,適用於類中所有public的方法
@Transactional
public class BbtForumImpl implements BbtForum {
//②提供額外的註解信息,它將覆蓋①處的類級註解
@Transactional(readOnly=true)
public Forum getForum(int forumId){
}
}
②處的方法註解提供了readOnly事務屬性的設置,它將覆蓋類級註解中默認的readOnly=false設置。
在不同的事務管理器
在一般情況下,一個應用僅需使用到一個事務管理器就可以了。如果希望在不同的地方使用不同的事務管理器,則可以通過如下的方法實現:
public class MultiForumService{
//①使用名爲topic的事務管理器
@Transactional("topic")
public void addTopic(Topic topic){
...
}
//②使用名爲forum的事務管理器
@Transactional("forum")
public void updateForum(Forum forum){
...
}
}
而topic和forum的事務管理器可以在XML中分別定義,如下所示:
<bean id="forumTxManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="forumDataSource" > <!-- ①使用不同的數據源 -->
<qualifier value="forum" /> <!-- ②爲事務管理器標識一個名字 -->
</bean>
<bean id="topicTxManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="topicDataSource" > <!-- ③使用不同的數據源 -->
<qualifier value="topic" /> <!-- ④爲事務管理器標識一個名字 -->
</bean>
在①處,爲事務管理器指定了一個數據源,每個事務管理器都可以綁定一個獨立的數據源。在②處,指定了一個可被@Transactional註解引用的事務管理器標識。
在一兩處使用帶標識的@Transactional也許挺適合的,但如果到處都使用,則顯得比較哆嗦。可以自定義一個綁定到特定事務管理器的註解,然後直接使用這個自定義的註解進行標識:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("forum")//①綁定到forum的事務管理器中
public @interface ForumTransactional{
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("topic")//②綁定到topic的事務管理器中
public @interface TopicTransactional{
}
完成定義後,就可以用以下的方式對原來的代碼進行調整了:
public class MultiForumService{
//①使用名爲topic的事務管理器
@TopicTransactional
public void addTopic(Topic topic){
...
}
//②使用名爲forum的事務管理器
@ForumTransactional
public void updateForum(Forum forum){
...
}
}
- 六.Spring MVC
- 1.配置
在web.xml中添加以下配置:
<context-param><!-- ① -->
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/applicationContext-*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet><!-- ② -->
<servlet-name>ierp</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping><!-- ③ -->
<servlet-name>ierp</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
在①處,通過contextConfigLocation參數指定業務層Spring容器的配置文件(多個配置文件使用逗號分隔),ContextLoaderListener是一個ServletContextListener,它通過contextConfigLocation參數所指定的Spring配置文件啓動”業務層”的Spring容器.
在②處,配置了名爲ierp的DispatcherServlet,它默認自動加載/WEB-INF/ierp-servlet.xml(即<servlet-name>-servlet.xml)的Spring配置文件,啓動Web層的Spring容器。
在③處通過<servlet-mapping>指定DispatcherServlet處理所有URL的HTTP請求。
我們知道多個Spring容器之間可設置爲父子級的關係,實現良好的解耦。在這裏,”Web層”Spring容器將作爲”業務層”Spring容器的子容器,退”Web層”容器可以引用“業務層”窗口的Bean,而“業務層”容器卻訪問不到”Web層”容器的Bean。
需要提醒的是,一個web.xml可以配置多個DispatcherServlet,通過其<servlet-mapping>的配置,讓每個DispatcherServlet處理不同的請求。
DispatcherServlet遵循“契約優於配置”的原則,在大多數情況下,你無須進行額外的配置,只需按契約行事即可。
如果你確實要對DispatcherServlet的默認規則進行調整,DispatcherServlet是敞開胸懷的。下面是常用的一些配置參數,可以通過<servlet>的<init-param>指定。
- namespace:DispatcherServlet對應的命名空間,默認爲”<servlet-name>”,用以構造Spring配置文件的路徑。顯式指定該屬性後,配置文件對應的路徑爲:WEB-INF/<namespace>.xml而非WEB-INF/<servlet-name>-servlet.xml。如果將namespace設置爲ierp2,則對應的Spring配置文件爲WEB-INF/ierp2.xml。
- contextConfigLocation:如果DispatcherServlet上下文對就的Spring配置文件有多個,則可使用該屬性按照Spring資源路徑的方式指定。如”classpath:ierp.xml,classpath:ierp2.xml”,DispatcherServlet將使用類路徑下的ierp.xml和ierp2.xml這兩個配置文件初始化WebApplicationContext。
- publishContext:boolean類型屬性,默認值爲true。DispatcherServlet根據該屬性決定是否將WebApplicationContext發佈到ServletContext的屬性列表中,以便調用者可藉由ServletContext找到WebApplicationContext初值,對應的屬性名爲DispatcherServlet#getServletContextAttributeName()返回的值。
- publishEvents:boolean類型屬性。當DispatcherServlet處理完一個請求後,是否需要身容器發佈一個ServletRequestHandledEvent事件,默認爲true。如果窗口中沒有任何事件監聽器,可以將些屬性設置爲false,以便提高運行性能。
在Spring Web上下文中配置FreeMarker
配置如下:
<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer"
p:templateLoaderPath="/WEB-INF/template" 1-a
p:defaultEncoding="UTF-8" > 1-b
<property name="freemarkerSettings"> 1-c
<props>
<prop key="classic_compatible">true</prop>
</props>
</property>
</bean>
<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver"
p:order="5" 2-a
p:suffix=".ftl" 2-b
p:contentType="text/html; charset=utf-8" /> 2-c
在1處,我通過FreeMarkerConfigurer配置了FreeMarker的環境,首先在1-a處指定了模版文件的存放路徑.由於我們的模版文件採用UTF-8編碼格式,所以必須顯工配置defaultEncoding屬性,否則將採用系統默認的編碼,造成亂碼的現象.
FreeMarker擁有豐富的自定義屬性,用戶可以通過freemarkerSettings進行統一的設置,需要閱讀FreeMarker的參考文檔,以獲取可配置的屬性信息,這些屬性名稱都採用小寫並用”_”連接,1-c處配置的class_compatible屬性非常重要,否則當FreeMarker模版碰到值爲null的對象屬性時,將拋出異常.將classic_compatible設置爲true後,FreeMarker將採購類似於JSTL標籤的行爲處理模型屬性數據:返回一個空白字符串,而非拋出系統異常.
在搭建好FreeMarker的環境後,就可以通過FreeMarkerViewResolver視圖解析器將”userListFtl”的邏輯視圖名解析爲一個對應的FreeMarkerView視圖對象.
2-a外指定該視圖解析器的優先級,它將優先於InteralResourceViewResolver執行(因爲InteralResourceViewResolver默認的優先級最低).而2-b處指定了後綴,這新”userListFtl”的邏輯視圖名將解析爲”/WEB-INF/template/userListFtl.ftl”的視圖對象。由於userListFtl.ftl模版最終產生的html,且我們希望使用UTF-8編碼格式輸出內容,所以需要通過2-c處的contentType屬性進行相應配置,如果不指定該屬性,輸出的HTML將會產生中文亂碼的問題。
- 2.DispatcherServlet內部邏輯
現在有一個問題:Spring如何將上下文中的SpringMVC組件裝配到DispatcherServlet中?通過查看DispatcherServlet的initStrategies()的代碼,一切就真相大白了:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);//①初始化上傳文件解析器(直譯爲多部分解析器)
initLocaleResolver(context);//②初始化本地化解析器
initThemeResolver(context);//③初始化主題解析器
initHandlerMappings(context);//④初始處理器映射器
initHandlerAdapters(context);//⑤初始化處理器適配器
initHandlerExceptionResolvers(context);//⑥初始化處理器異常解析器
initRequestToViewNameTranslator(context);//⑦初始化請求到視圖名翻譯器
initViewResolvers(context);//⑧初始化視圖解析器
}
initStrategies()方法將在WebApplicationContext初始化後自動執行,些時Spring上下文中的Bean已經初始化完畢。該方法的工作是通過反射機制查找並裝配Spring容器中用戶顯式自定義的組件Bean,如果找不到再裝配默認的組件實例。
Spring MVC定義了一套默認的組件實現類,也就是說即使在Spring窗口中沒有顯工定義組件Bean,DispatcherServlet也會裝配好一套可用的默認組件。在org.springframework.web.servlet.jar包的org.springframework.web.servlet包路徑下擁有一個DispatcherServlet.properties配置文件,該文件指定了DispatcherServletr所使用的默認組件。
##本地化解析器
org.springframework.web.servlet.LocaleResolver=
org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
##主題解析器
org.springframework.web.servlet.ThemeResolver=
org.springframework.web.servlet.theme.FixedThemeResolver
##處理器映射
org.springframework.web.servlet.HandlerMapping=
org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
##處理器適配器
org.springframework.web.servlet.HandlerAdapter=
org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
##異常處理器
org.springframework.web.servlet.HandlerExceptionResolver=
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
##視圖名稱翻譯器
org.springframework.web.servlet.RequestToViewNameTranslator=
org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
##視圖解析器
org.springframework.web.servlet.ViewResolver=
org.springframework.web.servlet.view.InternalResourceViewResolver
如果用戶希望採用非默認類型的組件,則只需在Spring配置文件中配置自定義的組件Bean即可,Spring MVC一旦發現上下文中擁有用戶自定義的組件,就不會使用默認的組件。概言之,當DispatcherServlet初始化後,就會自動掃描上下文的Bean,根據名稱或類型匹配的機制查找自定義的組件,找不到時則使用DispatcherServlet.properties定義的默認組件。通過下表可以進一步深入瞭解DispatcherServlet裝配每種組件的過程:
組件類型 |
發現機制 |
文件上傳解析器 (☆) |
如果用戶沒有在上下文中顯示定義這一類型的組件,DispatcherServlet中將不會擁有該類型的組件 |
本地化解析器 (☆) |
|
主題解析器 (☆) |
|
處理器映射器 (★) |
|
處理器適配器 (★) |
|
處理器異常解析器 (★) |
|
視圖名翻譯器 (☆) |
|
視圖解析器 (★) |
|
有些組件最多隻允許一個實例,如MultipartResolver、LocaleResolver等,上表使用☆進行標註。而另一類型的組件允許存在多個,如HandlerMapping、HandlerAdapter等,上表使用★進行標註。同一類型的組件如果存在多個,它們之間的優先級順序如何確定呢?這些組件都實現了orag.springframework.core.Ordered接口,可通過order屬性確定優先順序,值越小優先級越高。
- 3.MVC註解
- @Controller
- @RequestMapping
- @PathVariable
- @RequestParam
- @CookieValue
- @RequestHeader
- @SessionAttributes
- @ModelAttribute
- 4.HttpMessageConverter<T>
- 5.@ResponseBody、@RequestBody、HttpEntity、ResponseEntity、RestTemplate
- 6.返回Json
- Spring XML配置
Spring MVC提供了幾個處理XML\JSON格式的請求/響應消息的HttpMessageConverter。MappingJacksonHttpMessageConverter是處理JSON格式的請求或響應消息的,下面我們就JSON格式舉個例子,在Spring XML配置文件中添加以下配置:
<bean
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<!-- 之前默認的 --> 1
<bean class="org.springframework.http.converter.BufferedImageHttpMessageConverter" />
<bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />
<bean class="org.springframework.http.converter.StringHttpMessageConverter" />
<bean class="org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter" />
<!-- 添加處理JSON的 --> 2-a
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes"> 2-b
<list>
<value>application/json;charset=UTF-8</value> 2-c
</list>
</property>
</bean>
</list>
</property>
</bean>
1處配置的AnnotationMethodHandlerAdapter默認的加載的HttpMessageConverter,2-a添加處理JSON格式的請求或響應消息。2-b是表示響應的MediaType類型,2-c處添加JSON格式的類型。
裝配好處理JSON的HttpMessageConverter後,我們在控制器中編寫相應的方法,請看下面。
- 代碼實現
package com.sage.ierp.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.sage.jfianl.model.User;
@Controller
public class JsonController {
@ModelAttribute("user")
public User getUser(){
User user = new User();
user.setName("Saleson Lee");
user.setAge(25);
return user;
}
@RequestMapping("json")
public @ResponseBody User json(@ModelAttribute("user") User user){
return user;
}
@RequestMapping("json2")
public ResponseEntity<User> json2(@ModelAttribute("user") User user){
return new ResponseEntity<User>(user, HttpStatus.OK);
}
}
- 7.處理模型數據
- 8.攔截器
- 七.Spring 上傳附件
Ibates
- 一.Ibates 配置
- 二.Ibates CRUD
- 三.Ibates事務管理
- 四.Ibates 分佈
- 五.Spring MVC + Ibates 架構
測試
- 一.Junit + Spring 測試
- 二.Junit + Spring + Ibates 測試
- 三.Junt + Spring MVC 測試