Spring mvc mybaties druid 搭建多數據源

今天閒來無時,練習了一下Spring mvc +mybaties + druid 搭建多數據源。

下面使用了mysql 和 postgresql 爲例,話不多說,直接看代碼:


配置文件

jdbc.properties

mysql.jdbc.driverClassName=com.mysql.jdbc.Driver
mysql.jdbc.url=jdbc:mysql://localhost:3306/jazz?characterEncoding=utf-8
mysql.jdbc.username=root
mysql.jdbc.password=root

postgresql.jdbc.driverClassName=org.postgresql.Driver
postgresql.jdbc.url=jdbc:postgresql://localhost:5432/BJC20161122
postgresql.jdbc.username=myerp
postgresql.jdbc.password=godblessme

spring-mvc-applicatonContext.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:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
			http://www.springframework.org/schema/task  http://www.springframework.org/schema/task/spring-task-3.2.xsd"
      >
    <!-- 定義受環境影響易變的變量,例如數據庫配置屬性文件,應用程序屬性文件等 -->
	<context:property-placeholder location="classpath:*.properties"/>
	
	<!-- component-scan自動搜索@Component , @Controller , @Service , @Repository等標註的類 -->
	<context:component-scan base-package="com.**.service,bean" />
	
    <!-- 配置DataSource數據源 -->
    <bean id="mysqlDataSourceSpied" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 數據庫連接 -->
		<property name="driverClassName" value="${mysql.jdbc.driverClassName}" />
		<property name="url" value="${mysql.jdbc.url}" />
		<property name="username" value="${mysql.jdbc.username}" />
		<property name="password" value="${mysql.jdbc.password}" />
		
		<property name="filters" value="stat"/>
		<!-- 連接池設置 -->
		<property name="initialSize" value="2" />
		<property name="maxActive" value="100" />
		<property name="maxWait" value="30000" />
		<property name="poolPreparedStatements" value="false" />
		<property name="defaultAutoCommit" value="false" />
		
		<property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <property name="minEvictableIdleTimeMillis" value="300000"/>

        <property name="validationQuery" value="SELECT 'x' from dual"/>
        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>
    </bean>
    
	<bean id="mysqlDataSource" class="net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy">
		<constructor-arg ref="mysqlDataSourceSpied" />
	</bean>
	
	<!-- 配置DataSource數據源 -->
    <bean id="postgresqlDataSourceSpied" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 數據庫連接 -->
		<property name="driverClassName" value="${postgresql.jdbc.driverClassName}" />
		<property name="url" value="${postgresql.jdbc.url}" />
		<property name="username" value="${postgresql.jdbc.username}" />
		<property name="password" value="${postgresql.jdbc.password}" />
		
		<property name="filters" value="stat"/>
		<!-- 連接池設置 -->
		<property name="initialSize" value="2" />
		<property name="maxActive" value="100" />
		<property name="maxWait" value="30000" />
		<property name="poolPreparedStatements" value="false" />
		<property name="defaultAutoCommit" value="false" />
		
		<property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <property name="minEvictableIdleTimeMillis" value="300000"/>

        <property name="validationQuery" value="SELECT 1"/>
        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>
    </bean>
    
	<bean id="postgresqlDataSource" class="net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy">
		<constructor-arg ref="postgresqlDataSourceSpied" />
	</bean>
	
	<bean id="dataSource" class="com.jazz.first.datasource.DynamicDataSource">
        <property name="defaultTargetDataSource" ref="mysqlDataSource" />
        <property name="targetDataSources">
            <map key-type="com.jazz.first.datasource.SourcesEnum">
                <entry key="mysql" value-ref="mysqlDataSource"/>
                <entry key="postgresql" value-ref="postgresqlDataSource"/>
                <!-- 這裏還可以加多個dataSource -->
            </map>
        </property>
    </bean>   
	
    <!-- SqlSesstion Factory 定義 ↓ mybatis文件 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<!-- 自動掃描entity目錄,省略Configuration.xml裏手工配置 -->
		<property name="mapperLocations" value="classpath*:/com/**/dao/mapper/*Mapper.xml"/>
		<property name="plugins">
			<array>
				<bean class="com.github.pagehelper.PageHelper">
					<property name="properties">
						<value>
							pageSizeZero=true
							rowBoundsWithCount=true
							reasonable=true
							offsetAsPageNum=true
							params=pageNum=offset;pageSize=limit;
							supportMethodsArguments=true
							returnPageInfo=check
						</value>
					</property>
				</bean>
			</array>
		</property>
	</bean>
	<!-- SqlSession Factory 定義 ↑ -->

	<!-- MapperScannerConfigurer定義,用於掃描Mapper DAO類 ↓ -->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com.**.dao.**"/>
		<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
	</bean>
    <!-- MapperScannerConfigurer定義,用於掃描Mapper DAO類 ↑ -->
	
	<!-- 數據庫事務管理 -->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- ↓ ↓ 使用註解事務 ↓ ↓ -->
	<!-- <tx:annotation-driven transaction-manager="prjTxManager" proxy-target-class="true"/> -->
	<!-- ↑ ↑ 使用註解事務 ↑ ↑  -->
    
    <tx:advice id="txAdvice" transaction-manager="transactionManager">   	
        <tx:attributes>
            <tx:method name="create*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
            <tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
            <tx:method name="add*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
            <tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
            <tx:method name="delete*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
            <tx:method name="crud*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
            <tx:method name="schedule*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
            <tx:method name="do*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
            <tx:method name="*" />
        </tx:attributes>
    </tx:advice>
    
    <aop:aspectj-autoproxy proxy-target-class="true" />
    <aop:config>
        <aop:advisor id="managerTx" advice-ref="txAdvice" pointcut="execution(* *..service..*.*(..))" order="2"/>
    </aop:config>
    
    <bean id="dataSourceAspect" class="com.jazz.first.datasource.DataSourceAspect" />    
	<aop:config proxy-target-class="true">    
	    <aop:aspect id="dataSourceAspect" ref="dataSourceAspect" order="1">    
	        <aop:pointcut id="tx" expression="execution(* *..service..*.*(..)) "/>    
	        <aop:before pointcut-ref="tx" method="before" />                
	    </aop:aspect>    
	</aop:config>
   
	<!-- 公共dao -->
	<context:component-scan base-package="com.**.dao" />
</beans>

注意:由於我使用的註解式事務,和我們的AOP數據源切面有一個順序的關係。數據源切換必須先執行,數據庫事務才能獲取到正確的數據源。所以要明確指定 註解式事務 order=2和 我們AOP數據源切面order=1。


java代碼

AbstractRoutingDataSource 是spring提供的一個多數據源抽象類。spring會在使用事務的地方來調用此類的determineCurrentLookupKey()方法來獲取數據源的key值。我們繼承此抽象類並實現此方法:

package com.jazz.first.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {  
 
    @Override  
    protected Object determineCurrentLookupKey() {
    	
    	return HandleDataSource.getDataSource();  
    }  
 
}

DataSourceRouter 類中通過HandleDataSource.getDataSource()獲取數據源的key值。此方法應該和線程綁定。

package com.jazz.first.datasource;

/**
 * 線程相關的數據源處理類
 * @author 
 */
public class HandleDataSource {
	
	// 數據源名稱線程池
	private static final ThreadLocal<SourcesEnum> contextHolder = new ThreadLocal<SourcesEnum>();
	
	/**
	 * 獲取數據源
	 * @return 數據源名稱
	 */
	public static void setDataSource(SourcesEnum dbType) {  
           contextHolder.set(dbType);  
    }
	
	/**
	  * 獲取數據源
	  * @return 數據源名稱
	  */
    public static SourcesEnum getDataSource() {  
           return contextHolder.get();  
    }  
    
    /**
     * 清空數據源
     */
    public static void clearDbType() {  
           contextHolder.remove();  
    }
}

對於spring來說,註解即簡單方便且可讀性也高。所以,我們也通過註解在service的方法前指定所用的數據源。我們先定義自己的註解類,其中value爲數據源的key值。

package com.jazz.first.datasource;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 數據源註解類
 * @author 
 *
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    SourcesEnum value();
}

通過枚舉來對應不同的數據源,這裏一定得與配置文件中的key相同。

package com.jazz.first.datasource;

public enum SourcesEnum {
	mysql,postgresql
}

最後,我們可以通過AOP攔截所有service方法,在方法執行之前獲取方法上的註解:即數據源的key值。

package com.jazz.first.datasource;

import java.lang.reflect.Method;
import java.text.MessageFormat;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * 切換數據源(不同方法調用不同數據源)
 */
@Aspect
@Component
@Order(1) //請注意:這裏order一定要小於tx:annotation-driven的order,即先執行DataSourceAspect切面,再執行事務切面,才能獲取到最終的數據源
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DataSourceAspect {
    static Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);

    /**
     * 切入點 service包及子孫包下的所有類
     */
    @Pointcut("execution(* *..service..*.*(..))")
    public void aspect() {
    }

    /**
     * 配置前置通知,使用在方法aspect()上註冊的切入點
     */
    @Before("aspect()")
    public void before(JoinPoint point) {
        Class<?> target = point.getTarget().getClass();
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod() ;
        DataSource dataSource = null ;
        //從類初始化
        dataSource = this.getDataSource(target, method) ;
        //從接口初始化
        if(dataSource == null){
            for (Class<?> clazz : target.getInterfaces()) {
                dataSource = getDataSource(clazz, method);
                if(dataSource != null){
                    break ;//從某個接口中一旦發現註解,不再循環
                }
            }
        }

        if(dataSource != null && !StringUtils.isEmpty(dataSource.value()) ){
            HandleDataSource.setDataSource(dataSource.value());
        }
    }

    @After("aspect()")
    public void after(JoinPoint point) {
        //使用完記得清空
        HandleDataSource.setDataSource(null);
    }


    /**
     * 獲取方法或類的註解對象DataSource
     * @param target      類class
     * @param method    方法
     * @return DataSource
     */
    public DataSource getDataSource(Class<?> target, Method method){
        try {
            //1.優先方法註解
            Class<?>[] types = method.getParameterTypes();
            Method m = target.getMethod(method.getName(), types);
            if (m != null && m.isAnnotationPresent(DataSource.class)) {
                return m.getAnnotation(DataSource.class);
            }
            //2.其次類註解
            if (target.isAnnotationPresent(DataSource.class)) {
                return target.getAnnotation(DataSource.class);
            }

        } catch (Exception e) {
            e.printStackTrace();
            logger.error(MessageFormat.format("通過註解切換數據源時發生異常[class={0},method={1}]:"
                    , target.getName(), method.getName()),e)  ;
        }
        return null ;
    }
}

這樣多數據源就搭建起來了,我們只需要在每個service方法前使用@DataSource("數據源key")註解即可。



總結:此框架弊端顯而易見,就是在一個service方法中只能訪問同一個數據庫,不能進行切換。當然好處事比較靈活,可以配置多個數據源,方式一致,service方法上加上註解後則切換到指定數據源,方法執行完後,則回答原始數據源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章