springBoot jpa 多數據源的動態切換

    這周笨小蔥一直在研究如何使用springBoot的多數據源配置。

    看了3天的網上的博客,發現大多數都是多數據源的配置,並沒有很詳細的關於使用springBoot的多數據源動態切換的配置。前者整體配置過程是在springBoot的原有的jpa實體管理工廠(entityManagerFactory)的基礎上(這裏,entityManagerFactory會綁定一個數據源,而transactionManager只需將entityManagerFactory注入就可以綁定數據源了)再次創建一個實體類管理工廠,然後綁定另外一個數據源,但是各自entityManagerFactory都需要綁定各自的repository。這種配置適合一個用戶操作不同的數據庫。而如果要不同的用戶操作不同的數據源,同時對應同一個repository。那麼就不能夠實現啦。所以需要實現數據源的動態切換。

    這裏第一種配置,笨小蔥就不詳解了,網上有很多資料。

    詳細說一下,關於springBoot jpa的多數據源動態切換

    wKioL1TxIC-gAVlAAABpRIiqH-w927.jpg

      這裏我們配置2個數據源,通過動態數據源來切換,對應一個entityManagerFactory,一個repository。主要功能是不同的用戶操作不同的數據庫,但是他們的數據庫結構是一樣的,所調用的controller方法也是一樣的。

     實現原理:

      1、擴展Spring的AbstractRoutingDataSource抽象類(該類充當了DataSource的路由中介, 能有在運行時, 根據某種key值來動態切換到真正的DataSource上。)

    從AbstractRoutingDataSource的源碼中:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean
 

    我們可以看到,它繼承了AbstractDataSource,而AbstractDataSource不就是javax.sql.DataSource的子類,So我們可以分析下它的getConnection方法:


public Connection getConnection() throws SQLException {  
    return determineTargetDataSource().getConnection();  
}  
   
public Connection getConnection(String username, String password) throws SQLException {  
     return determineTargetDataSource().getConnection(username, password);  
}

 

    獲取連接的方法中,重點是determineTargetDataSource()方法,看源碼:


/** 
     * Retrieve the current target DataSource. Determines the 
     * {@link #determineCurrentLookupKey() current lookup key}, performs 
     * a lookup in the {@link #setTargetDataSources targetDataSources} map, 
     * falls back to the specified 
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary. 
     * @see #determineCurrentLookupKey() 
     */  
    protected DataSource determineTargetDataSource() {  
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");  
        Object lookupKey = determineCurrentLookupKey();  
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);  
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {  
            dataSource = this.resolvedDefaultDataSource;  
        }  
        if (dataSource == null) {  
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");  
        }  
        return dataSource;  
    }

 

    上面這段源碼的重點在於determineCurrentLookupKey()方法,這是AbstractRoutingDataSource類中的一個抽象方法,而它的返回值是你所要用的數據源dataSource的key值,有了這個key值,resolvedDataSource(這是個map,由配置文件中設置好後存入的)就從中取出對應的DataSource,如果找不到,就用配置默認的數據源。

    看完源碼,應該有點啓發了吧,沒錯!你要擴展AbstractRoutingDataSource類,並重寫其中的determineCurrentLookupKey()方法,來實現數據源的切換:


package com.datasource.test.util.database;
 
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
 
/**
 * 獲取數據源(依賴於spring)
 * @author linhy
 */
public class DynamicDataSource extends AbstractRoutingDataSource{
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceHolder.getDataSource();
    }
}

 

    DataSourceHolder這個類則是我們自己封裝的對數據源進行操作的類:


package com.datasource.test.util.database;
 
/**
 * 數據源操作
 * @author linhy
 */
public class DataSourceHolder {
    //線程本地環境
    private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();
    //設置數據源
    public static void setDataSource(String customerType) {
        dataSources.set(customerType);
    }
    //獲取數據源
    public static String getDataSource() {
        return (String) dataSources.get();
    }
    //清除數據源
    public static void clearDataSource() {
        dataSources.remove();
    }
 
}

創建完這2個類之後,只需要在調用controller的方法之前調用對應的數據源就可以了。調用數據源即:

DataSourceHolder.setDataSource (xxxx);
這樣在執行controller方法之前就完成了當前線程(http請求)的數據源切換。


這個方法也是參考的網上的。但是如何將其整合入springBoot中,還需要自己調試一下。笨小蔥花了2天時間,終於配置調試好了。下面配上springBoot的各個文件。

首先:創建上面的2個類。

DynamicDataSource.java :

package cc.study.springboot.domain;


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


/**
 * Created by Administrator on 2015/11/25.
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {


        return DataSourceHolder.getDataSource();
    }
}


DataSourceHolder.java :

package cc.study.springboot.domain;

/**
 * Created by Administrator on 2015/11/25.
 */
public class DataSourceHolder {

    //線程本地環境
    private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();
    //設置數據源
    public static void setDataSource(String customerType) {
        dataSources.set(customerType);
    }
    //獲取數據源
    public static String getDataSource() {
        return (String) dataSources.get();
    }
    //清除數據源
   public static void clearDataSource() {
        dataSources.remove();
    }
}

然後是多數據源與動態數據源的配置,以及entityManagerFactory和transactionManager的配置文件

application-data.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:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"
       >


    <bean id="ds1" name="ds1"
          class="org.springframework.jdbc.datasource.DriverManagerDataSource" primary="true">  //這裏多數據源,springBoot啓動時需要指定一個默認的數據源,所以需要加primary="true",否則會出現數據源bean匹配失敗錯誤
        <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
        <property name="url" value="jdbc:sqlserver://127.0.0.1:1433;databaseName=xxx"/>
        <property name="username" value="test"/>
        <property name="password" value="xxx"/>
    </bean>

    <bean id="ds2" name="ds2"
          class="org.springframework.jdbc.datasource.DriverManagerDataSource" >
        <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
        <property name="url" value="jdbc:sqlserver://127.0.0.1:1433;databaseName=cc"/>
        <property name="username" value="test"/>
        <property name="password" value="xxx"/>
    </bean>

    <!--動態選擇數據源-->
    <bean id="dataSource" class="cc.study.springboot.domain.DynamicDataSource" >
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <entry key="1" value-ref="ds1"/>
                <entry key="2" value-ref="ds2"/>
            </map>
        </property>
       <property name="defaultTargetDataSource" ref="ds1"/> //不可少
    </bean>

    <bean id="entityManagerFactory"
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
          destroy-method="destroy" >
        <property name="dataSource" ref="dataSource" />     //這裏將動態數據源bean注入
        <property name="jpaVendorAdapter"> 
            <bean
                    class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="databasePlatform" value="org.hibernate.dialect.SQLServer2008Dialect"/>
                <property name="showSql" value="true"/>
            </bean>
        </property>
        <property name="packagesToScan" value="cc.study.springboot.domain"/>
        <property name="jpaPropertyMap">
            <map>
                <entry key="javax.persistence.schema-generation.database.action" value="none"/>
            </map>
        </property>
    </bean>

    <bean id="transactionManager"
          class="org.springframework.orm.jpa.JpaTransactionManager">

        <property name="entityManagerFactory"
                  ref="entityManagerFactory"/>
    </bean>

    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>

配置完成後,需要讓springBoot在啓動時候,創建application上下文對象的時候加載這個xml文檔,創建數據源bean

package cc.study.springboot;

import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;


/*@SpringBootApplication is a convenience annotation that adds all of the following:
    @Configuration tags the class as a source of bean definitions for the application context.
    @EnableAutoConfiguration. This annotation tells Spring Boot to “guess” how you will want to configure Spring,
     based on the jar dependencies that you have added. Since spring-boot-starter-web added Tomcat and Spring MVC,
     the auto-configuration will assume that you are developing a web application and setup Spring accordingly.
     This flags the application as a web application and activates key behaviors such as setting up a DispatcherServlet.
    @ComponentScan tells Spring to look for other components, configurations, and services in the the hello package, allowing it to find the HelloController.*/ </span><span style="white-space:pre">	
@Configuration
@Configurable(autowire = Autowire.BY_NAME)    //定義bean的注入方式
@EnableAutoConfiguration
@ComponentScan
@ImportResource("classpath:application-data.xml")
class Application {

    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(Application.class, args);
       

    }
}

下面需要在調用controller中的方法之前,添加一個aop切面。用於根據需求修改數據源。

package cc.study.springboot.service;

import cc.study.springboot.domain.DataSourceHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;

/**
 * Created by Administrator on 2015/12/16.
 */
@Aspect     //註解的方式配置aop
@Configuration
public class dataSourceAspect {
    @Pointcut("execution(* cc.study.springboot.controller..*.*(..))")
    private void anyMethod(){}//定義一個切入點

    @Before("anyMethod()")
    public void dataSourceChange()
    {
        System.out.print("更改數據源爲cc");
        DataSourceHolder.setDataSource("2");
        /*這裏根據用戶的類型來更改對應的數據源*/
    }
}

下面附上controller,domain和repository的代碼

UserInfoController.java:

package cc.study.springboot.controller;


import cc.study.springboot.domain.DataSourceHolder;
import cc.study.springboot.domain.User;
import cc.study.springboot.repository.UserRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;

@RestController
public class UserInfoController {

    @Inject
    private UserRepository repo;



    @Value("${datasource.secondary.url}")
    private String url;

    @RequestMapping(value = "/userInfo/{id}", method = RequestMethod.GET)

    public ResponseEntity<?> getUser(@PathVariable("id") String id,HttpServletRequest request) {



    /*  WebApplicationContext wct= WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext());
        ComboPooledDataSource ds= (ComboPooledDataSource) wct.getBean("dataSource");
        ds.setJdbcUrl(url);
*/
        User u = repo.findByUsername(id);

        return new ResponseEntity<Object>(u, HttpStatus.OK);
    }


    @RequestMapping(value = "/user", method = RequestMethod.GET)

    public ResponseEntity<?> userLogin() {


        DataSourceHolder.setDataSource("2");
        User u = repo.findByUsername("80045");
        return new ResponseEntity<Object>(u, HttpStatus.OK);
       /* try {

            if("0".equals(loginPost("http://192.168.0.69/SITApps/SITPortal/PortalPage/VerificationUser.aspx", "username=" + username + "&password=" + password)))
            {
                return new ResponseEntity<>( HttpStatus.OK);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);*/


    }


}


User.java:
package cc.study.springboot.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;

import javax.persistence.*;

@Entity
@Table(name = "t_mobile_person")
public class User {
    private String username;
    private String password;
    private String factoryCode;
    private String departmentCode;
    private String permissionSys;
    private String permissionMonitor;
    private String realname;



    @Id
    @Column(name = "person_id")
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Column(name = "password")
    @JsonIgnore
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Column(name = "factory_code")
    public String getFactoryCode() {
        return factoryCode;
    }

    public void setFactoryCode(String factoryCode) {
        this.factoryCode = factoryCode;
    }

    @Column(name = "department_code")
    public String getDepartmentCode() {
        return departmentCode;
    }

    public void setDepartmentCode(String departmentCode) {
        this.departmentCode = departmentCode;
    }

    @Column(name = "permission_sys")
    @JsonIgnore
    public String getPermissionSys() {
        return permissionSys;
    }

    public void setPermissionSys(String permissionSys) {
        this.permissionSys = permissionSys;
    }

    @Column(name = "permission_monitor")
    @JsonIgnore
    public String getPermissionMonitor() {
        return permissionMonitor;
    }

    public void setPermissionMonitor(String permissionMonitor) {
        this.permissionMonitor = permissionMonitor;
    }

    @Column(name = "REALNAME")
    public String getRealname() {
        return realname;
    }

    public void setRealname(String realname) {
        this.realname = realname;
    }


}


UserRepository.java:
package cc.study.springboot.repository;


import cc.study.springboot.domain.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
@Transactional
public interface UserRepository extends CrudRepository<User, String> {
    User findByUsername(String id);
}

ok,搞定了。後面我會傳上項目源碼。項目源碼:http://download.csdn.net/detail/sunshine920103/9379127


發佈了35 篇原創文章 · 獲贊 15 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章