Spring 實踐
標籤: Java與設計模式
Junit集成
前面多次用到@RunWith
與@ContextConfiguration
,在測試類添加這兩個註解,程序就會自動加載Spring配置並初始化Spring容器,方便Junit與Spring集成測試.使用這個功能需要在pom.xml中添加如下依賴:
- pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.2.0.RELEASE</version>
</dependency>
- 以
@RunWith
和@ContextConfiguration
加載Spring容器
/**
* Spring 整合 Junit
* Created by jifang on 15/12/9.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/applicationContext.xml")
public class BeanTest {
@Autowired
private Bean bean;
@Test
public void testConstruct() {
Car car = bean.getCar();
System.out.println(car);
}
}
Web集成
我們可以利用ServletContext
容器保存數據的唯一性, 以及ServletContextListener
會在容器初始化時只被調用一次的特性. 在web.xml中配置spring-web包下的ContextLoaderListener
來加載Spring配置文件/初始化Spring容器:
- pom.xml/spring-web
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.2.0.RELEASE</version>
</dependency>
- 配置監聽器(web.xml)
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
- 加載Spring配置文件
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext.xml</param-value>
</context-param>
附: 完整web.xml文件git地址.
- 測試Servlet
@WebServlet(urlPatterns = "/servlet")
public class Servlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
Bean bean = context.getBean("bean", Bean.class);
Car car = bean.getCar();
System.out.println(car);
}
}
在應用中,普通的JavaBean由Spring管理,可以使用
@Autowired
自動注入.但Filter
與Servlet
例外,他們都是由Servlet容器管理,因此其屬性不能用Spring注入,所以在實際項目中,一般都不會直接使用Servlet,而是用SpringMVC/WebX/Struts2之類的MVC框架以簡化開發,後面會有專門的博客介紹這類框架,在此就不做深入介紹了.
- 注: 運行Servlet不要忘記添加servlet-api依賴:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
文件加載
1. 引入properties
可以將需要經常修改的屬性參數值放到properties文件, 並在Spring文件中引入.
- db.properties
## Data Source
mysql.driver.class=com.mysql.jdbc.Driver
mysql.url=jdbc:mysql://host:port/db?useUnicode=true&characterEncoding=UTF8
mysql.user=user
mysql.password=password
注意: value後不能有空格.
1.1 property-placeholde引入
在Spring配置文件中使用<context:property-placeholder/>
標籤引入properties文件,XML文件可通過${key}
引用, Java可通過@Value("${key}")
引用:
- XML
<context:property-placeholder location="classpath:common.properties"/>
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="${mysql.driver.class}"/>
<property name="jdbcUrl" value="${mysql.url}"/>
<property name="username" value="${mysql.user}"/>
<property name="password" value="${mysql.password}"/>
</bean>
- Java
@Component
public class AccessLog {
@Value("${mysql.url}")
private String value;
// ...
}
1.2 PropertiesFactoryBean
引入
Spring提供了org.springframework.beans.factory.config.PropertiesFactoryBean
,以加載properties文件, 方便在JavaBean中注入properties屬性值.
- XML
<bean id="commonProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="locations">
<list>
<value>classpath*:common.properties</value>
</list>
</property>
</bean>
- Java
@Controller
public class Bean {
@Value("#{commonProperties['bean.properties.name']}")
private String name;
// ...
}
2. import其他Spring配置
如果Spring的配置項過多,可以按模塊將配置劃分多個配置文件(-datasource.xml/-dubbo-provider.xml/-bean.xml), 並由主配置applicationContext.xml文件引用他們,此時可用<import/>
標籤引入:
<import resource="applicationContext-bean.xml"/>
<import resource="applicationContext-dubbo-provider.xml"/>
<import resource="applicationContext-dubbo-consumer.xml"/>
事務管理
Spring事務管理高層抽象主要由
PlatformTransactionManager
/TransactionDefinition
/TransactionStatus
三個接口提供支持:
PlatformTransactionManager(事務管理器)
PlatformTransactionManager
的主要功能是事務管理,Spring爲不同的持久層框架提供了不同的PlatformTransactionManager
實現:
事務 | 描述 |
---|---|
DataSourceTransactionManager |
JDBCTemplate/MyBatis/iBatis持久化使用 |
HibernateTransactionManager |
Hibernate持久化使用 |
JpaTransactionManager |
JPA持久化使用 |
JdoTransactionManager |
JDO持久化使用 |
JtaTransactionManager |
JTA實現管理事務,一個事務跨越多個資源時使用 |
因此使用Spring管理事務,需要爲不同持久層配置不同事務管理器實現.
TransactionDefinition(事務定義信息)
TransactionDefinition
提供了對事務的相關配置, 如事務隔離級別/傳播行爲/只讀/超時等:
- 隔離級別(isolation)
爲解決事務併發引起的問題(髒讀/幻讀/不可重複讀),引入四個隔離級別:
隔離級別 | 描述 |
---|---|
DEFAULT |
使用數據庫默認的隔離級別 |
READ_UNCOMMITED |
讀未提交 |
READ_COMMITTED |
讀已提交(Oracle默認) |
REPEATABLE_READ |
可重複讀(MySQL默認) |
SERIALIZABLE |
串行化 |
關於事務隔離級別的討論, 可參考我的博客JDBC基礎-事務隔離級別部分.
- 傳播行爲(propagation)
傳播行爲不是數據庫的特性, 而是爲了在業務層解決兩個事務相互調用的問題:
傳播類型 | 描述 |
---|---|
REQUIRED |
支持當前事務,如果不存在就新建一個(默認) |
SUPPORTS |
支持當前事務,如果不存在就不使用事務 |
MANDATORY |
支持當前事務,如果不存在則拋出異常 |
REQUIRES_NEW |
如果有事務存在,則掛起當前事務新建一個 |
NOT_SUPPORTED |
以非事務方式運行,如果有事務存在則掛起當前事務 |
NEVER |
以非事務方式運行,如果有事務存在則拋出異常 |
NESTED |
如果當前事務存在,則嵌套事務執行(只對DataSourceTransactionManager 有效) |
- 超時時間(timeout)
- 只讀(read-only)
只讀事務, 不能執行INSERT
/UPDATE
/DELETE
操作.
TransactionStatus(事務狀態信息)
獲得事務執行過程中某一個時間點狀態.
聲明式事務管理
Spring聲明式事務管理:無需要修改原來代碼,只需要爲Spring添加配置(XML/Annotation),就可以爲目標代碼添加事務管理功能.
需求: 轉賬案例(使用MyBatis).
- AccountDAO
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fq.dao.AccountDAO">
<update id="transferIn">
UPDATE account
SET money = money + #{0}
WHERE name = #{1};
</update>
<update id="transferOut">
UPDATE account
SET money = money - #{0}
WHERE name = #{1};
</update>
</mapper>
/**
* @author jifang
* @since 16/3/3 上午11:16.
*/
public interface AccountDAO {
void transferIn(Double inMoney, String name);
void transferOut(Double outMoney, String name);
}
- Service
public interface AccountService {
void transfer(String from, String to, Double money);
}
@Service("service")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDAO dao;
@Override
public void transfer(String from, String to, Double money) {
dao.transferOut(money, from);
// 此處拋出異常, 沒有事務將導致數據不一致
int a = 1 / 0;
dao.transferIn(money, to);
}
}
- mybatis-configuration.xml/applicationContext-datasource.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 加載mapper映射文件 -->
<mappers>
<mapper resource="mybatis/mapper/AccountDAO.xml"/>
</mappers>
</configuration>
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:db.properties"/>
<!-- 配置數據源 -->
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="${mysql.driver.class}"/>
<property name="jdbcUrl" value="${mysql.url}"/>
<property name="username" value="${mysql.user}"/>
<property name="password" value="${mysql.password}"/>
<property name="maximumPoolSize" value="5"/>
<property name="maxLifetime" value="700000"/>
<property name="idleTimeout" value="600000"/>
<property name="connectionTimeout" value="10000"/>
<property name="dataSourceProperties">
<props>
<prop key="dataSourceClassName">com.mysql.jdbc.jdbc2.optional.MysqlDataSource</prop>
<prop key="cachePrepStmts">true</prop>
<prop key="prepStmtCacheSize">250</prop>
<prop key="prepStmtCacheSqlLimit">2048</prop>
</props>
</property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<constructor-arg ref="hikariConfig"/>
</bean>
<!-- 配置SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis/mybatis-configuration.xml"/>
</bean>
<!-- 基於包掃描的mapper配置 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.fq.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
</beans>
- applicationContext.xml(沒有事務)
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.fq.service"/>
<import resource="applicationContext-datasource.xml"/>
</beans>
- Client
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/applicationContext.xml")
public class SpringClient {
@Autowired
private AccountService service;
@Test
public void client() {
service.transfer("from", "to", 10D);
}
}
執行以上代碼, 將會導致數據前後不一致.
XML配置
Spring事務管理依賴AOP,而AOP需要定義切面(Advice+PointCut),在Spring內部提供了事務管理的默認Adviceorg.springframework.transaction.interceptor.TransactionInterceptor
,並且Spring爲了簡化事務配置,引入tx標籤:
- 引入tx的命名空間,配置Advice:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 事務配置屬性, 對什麼方法應用怎樣的配置, 成爲TransactionDefinition對象 -->
<tx:attributes>
<!--
name: 方法名, 支持通配符
isolation: 隔離級別
propagation: 傳播行爲
timeout: 超時時間
read-only: 是否只讀
rollback-for: 配置異常類型, 發生這些異常回滾事務
no-rollback-for: 配置異常類型, 發生這些異常不回滾事務
-->
<tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED" timeout="-1" read-only="false"/>
</tx:attributes>
</tx:advice>
- 配置切面
Spring事務管理Advice基於SpringAOP,因此使用<aop:advisor/>
配置:
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.fq.service.impl.AccountServiceImpl.*(..))"/>
</aop:config>
註解配置
使用註解配置事務, 可以省略切點的定義(因爲註解放置位置就已經確定了PointCut的置), 只需配置Advice即可:
- 激活註解事務管理功能
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
- 在需要管理事務的業務類/業務方法上添加
@Transactional
註解
@Override
@Transactional(transactionManager = "transactionManger", readOnly = true)
public void transfer(String from, String to, Double money) {
// ...
}
可以在註解
@Transactional
中配置與XML相同的事務屬性(isolation/propagation等).
實踐
更推薦使用XML方式來配置事務,實際開發時一般將事務集中配置管理. 另外, 事務的isolation/propagation一般默認的策略就已經足夠, 反而我們需要配置是否只讀(比如MySQL主從備份時,主庫一般提供讀寫操作,而從庫只提供讀操作), 因此其配置可以如下:
<!-- 配置聲明式事務 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 定義方法的過濾規則 -->
<tx:attributes>
<!-- 定義所有get開頭的方法都是隻讀的 -->
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="select*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 配置事務AOP -->
<aop:config>
<!-- 定義切點 -->
<aop:pointcut id="dao" expression="execution (* com.fq.core.dao.*.*(..))"/>
<!-- 爲切點定義通知 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="dao"/>
</aop:config>
- 主從
<tx:advice id="txAdvice_slave" transaction-manager="transactionManager_slave">
<!-- 定義方法的過濾規則 -->
<tx:attributes>
<tx:method name="*" read-only="true"/>
</tx:attributes>
</tx:advice>
<tx:advice id="txAdvice_master" transaction-manager="transactionManager_slave">
<!-- 定義方法的過濾規則 -->
<tx:attributes>
<!-- 定義所有get開頭的方法都是隻讀的 -->
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="select*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>