├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── java │ │ └── cloud │ │ │ ├── config │ │ │ └── ConstId.java │ │ │ ├── dao │ │ │ ├── StudentDao.java │ │ │ └── TenantInfoDao.java │ │ │ ├── util │ │ │ ├── JsonUtil.java │ │ │ ├── ResultCode.java │ │ │ ├── ResultGenerator.java │ │ │ └── Result.java │ │ │ ├── CloudApplication.java │ │ │ ├── service │ │ │ └── StudentService.java │ │ │ ├── tenant │ │ │ ├── MultiTenantIdentifierResolver.java │ │ │ ├── MultiTenantConnectionProviderImpl.java │ │ │ └── TenantDataSourceProvider.java │ │ │ ├── entity │ │ │ ├── Student.java │ │ │ └── TenantInfo.java │ │ │ └── controller │ │ │ └── HelloController.java │ └── resources │ │ └── application.properties └── test │ └── java │ └── cloud │ └── CloudApplicationTests.java ├── .gitignore ├── gradlew.bat ├── gradlew └── README.md /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanyuanxiaoyao/multi-tenant/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/cloud/config/ConstId.java: -------------------------------------------------------------------------------- 1 | package cloud.config; 2 | 3 | public class ConstId { 4 | 5 | public volatile static String Id = ""; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/cloud/dao/StudentDao.java: -------------------------------------------------------------------------------- 1 | package cloud.dao; 2 | 3 | import cloud.entity.Student; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface StudentDao extends JpaRepository {} 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-bin.zip 6 | -------------------------------------------------------------------------------- /src/main/java/cloud/dao/TenantInfoDao.java: -------------------------------------------------------------------------------- 1 | package cloud.dao; 2 | 3 | import cloud.entity.TenantInfo; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface TenantInfoDao extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/cloud/util/JsonUtil.java: -------------------------------------------------------------------------------- 1 | package cloud.util; 2 | 3 | import com.google.gson.Gson; 4 | 5 | public class JsonUtil { 6 | 7 | private static Gson gson = null; 8 | 9 | public static Gson getGson() { 10 | if (gson == null) { 11 | gson = new Gson(); 12 | } 13 | return gson; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | nbproject/private/ 21 | build/ 22 | nbbuild/ 23 | dist/ 24 | nbdist/ 25 | .nb-gradle/ -------------------------------------------------------------------------------- /src/main/java/cloud/CloudApplication.java: -------------------------------------------------------------------------------- 1 | package cloud; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class CloudApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(CloudApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/cloud/util/ResultCode.java: -------------------------------------------------------------------------------- 1 | package cloud.util; 2 | 3 | /** 4 | * 响应码枚举,参考HTTP状态码的语义 5 | */ 6 | public enum ResultCode { 7 | SUCCESS(200),//成功 8 | FAIL(400),//失败 9 | UNAUTHORIZED(401),//未认证(签名错误) 10 | NOT_FOUND(404),//接口不存在 11 | INTERNAL_SERVER_ERROR(500);//服务器内部错误 12 | 13 | public int code; 14 | 15 | ResultCode(int code) { 16 | this.code = code; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/cloud/service/StudentService.java: -------------------------------------------------------------------------------- 1 | package cloud.service; 2 | 3 | import cloud.dao.StudentDao; 4 | import cloud.entity.Student; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @Service 11 | public class StudentService { 12 | 13 | @Autowired 14 | private StudentDao studentDao; 15 | 16 | public List findAll() { 17 | return studentDao.findAll(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/cloud/tenant/MultiTenantIdentifierResolver.java: -------------------------------------------------------------------------------- 1 | package cloud.tenant; 2 | 3 | import cloud.config.ConstId; 4 | import org.hibernate.context.spi.CurrentTenantIdentifierResolver; 5 | 6 | /** 7 | * 这个类是由Hibernate提供的用于识别tenantId的类,当每次执行sql语句被拦截就会调用这个类中的方法来获取tenantId 8 | * @author lanyuanxiaoyao 9 | * @version 1.0 10 | */ 11 | public class MultiTenantIdentifierResolver implements CurrentTenantIdentifierResolver{ 12 | 13 | // 获取tenantId的逻辑在这个方法里面写 14 | @Override 15 | public String resolveCurrentTenantIdentifier() { 16 | if (!"".equals(ConstId.Id)){ 17 | return ConstId.Id; 18 | } 19 | return "Default"; 20 | } 21 | 22 | @Override 23 | public boolean validateExistingCurrentSessions() { 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Database 2 | spring.datasource.url=jdbc:postgresql://localhost:5432/cloud_config 3 | spring.datasource.username=lanyuanxiaoyao 4 | spring.datasource.password= 5 | spring.datasource.driver-class-name=org.postgresql.Driver 6 | 7 | # Hibernate 8 | spring.jpa.show-sql=true 9 | spring.jpa.properties.hibernate.multiTenancy=DATABASE 10 | spring.jpa.properties.hibernate.tenant_identifier_resolver=cloud.tenant.MultiTenantIdentifierResolver 11 | spring.jpa.properties.hibernate.multi_tenant_connection_provider=cloud.tenant.MultiTenantConnectionProviderImpl 12 | # spring.jpa.hibernate.ddl-auto=create-drop 13 | 14 | # Session 15 | spring.session.store-type=none 16 | 17 | # Spring Security 18 | security.basic.enabled=false 19 | security.user.name=root 20 | security.user.password=root -------------------------------------------------------------------------------- /src/main/java/cloud/entity/Student.java: -------------------------------------------------------------------------------- 1 | package cloud.entity; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.Id; 7 | 8 | @Entity 9 | public class Student { 10 | 11 | @Id 12 | @GeneratedValue 13 | private Integer sid; 14 | private String sname; 15 | 16 | public Integer getSid() { 17 | return sid; 18 | } 19 | 20 | public void setSid(Integer sid) { 21 | this.sid = sid; 22 | } 23 | 24 | public String getSname() { 25 | return sname; 26 | } 27 | 28 | public void setSname(String sname) { 29 | this.sname = sname; 30 | } 31 | 32 | public Student(){} 33 | 34 | public Student(Integer sid, String sname){ 35 | setSid(sid); 36 | setSname(sname); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/cloud/util/ResultGenerator.java: -------------------------------------------------------------------------------- 1 | package cloud.util; 2 | 3 | /** 4 | * 响应结果生成工具 5 | */ 6 | public class ResultGenerator { 7 | private static final String DEFAULT_SUCCESS_MESSAGE = "SUCCESS"; 8 | 9 | public static Result genSuccessResult() { 10 | return new Result() 11 | .setCode(ResultCode.SUCCESS) 12 | .setMessage(DEFAULT_SUCCESS_MESSAGE); 13 | } 14 | 15 | public static Result genSuccessResult(Object data) { 16 | return new Result() 17 | .setCode(ResultCode.SUCCESS) 18 | .setMessage(DEFAULT_SUCCESS_MESSAGE) 19 | .setData(data); 20 | } 21 | 22 | public static Result genFailResult(String message) { 23 | return new Result() 24 | .setCode(ResultCode.FAIL) 25 | .setMessage(message); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/cloud/CloudApplicationTests.java: -------------------------------------------------------------------------------- 1 | package cloud; 2 | 3 | import cloud.dao.StudentDao; 4 | import cloud.entity.Student; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | 11 | import java.util.List; 12 | 13 | @RunWith(SpringRunner.class) 14 | @SpringBootTest 15 | public class CloudApplicationTests { 16 | 17 | @Autowired 18 | private StudentDao studentDao; 19 | 20 | @Test 21 | public void contextLoads() { 22 | 23 | studentDao.save(new Student(1,"zhangsan")); 24 | studentDao.save(new Student(2,"lisi")); 25 | 26 | List studentList = studentDao.findAll(); 27 | System.out.println(studentList.size()); 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/cloud/tenant/MultiTenantConnectionProviderImpl.java: -------------------------------------------------------------------------------- 1 | package cloud.tenant; 2 | 3 | import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl; 4 | 5 | import javax.sql.DataSource; 6 | 7 | /** 8 | * 这个类是Hibernate框架拦截sql语句并在执行sql语句之前更换数据源提供的类 9 | * @author lanyuanxiaoyao 10 | * @version 1.0 11 | */ 12 | public class MultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl { 13 | 14 | // 在没有提供tenantId的情况下返回默认数据源 15 | @Override 16 | protected DataSource selectAnyDataSource() { 17 | return TenantDataSourceProvider.getTenantDataSource("Default"); 18 | } 19 | 20 | // 提供了tenantId的话就根据ID来返回数据源 21 | @Override 22 | protected DataSource selectDataSource(String tenantIdentifier) { 23 | return TenantDataSourceProvider.getTenantDataSource(tenantIdentifier); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/cloud/util/Result.java: -------------------------------------------------------------------------------- 1 | package cloud.util; 2 | 3 | /** 4 | * 统一API响应结果封装 5 | */ 6 | public class Result { 7 | private int code; 8 | private String message; 9 | private Object data; 10 | 11 | public Result setCode(ResultCode resultCode) { 12 | this.code = resultCode.code; 13 | return this; 14 | } 15 | 16 | public int getCode() { 17 | return code; 18 | } 19 | 20 | public Result setCode(int code) { 21 | this.code = code; 22 | return this; 23 | } 24 | 25 | public String getMessage() { 26 | return message; 27 | } 28 | 29 | public Result setMessage(String message) { 30 | this.message = message; 31 | return this; 32 | } 33 | 34 | public Object getData() { 35 | return data; 36 | } 37 | 38 | public Result setData(Object data) { 39 | this.data = data; 40 | return this; 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | return JsonUtil.getGson().toJson(this); 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/cloud/controller/HelloController.java: -------------------------------------------------------------------------------- 1 | package cloud.controller; 2 | 3 | import cloud.config.ConstId; 4 | import cloud.dao.TenantInfoDao; 5 | import cloud.entity.TenantInfo; 6 | import cloud.service.StudentService; 7 | import cloud.tenant.TenantDataSourceProvider; 8 | import cloud.util.Result; 9 | import cloud.util.ResultGenerator; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.data.repository.query.Param; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import java.util.List; 16 | 17 | @RestController 18 | @RequestMapping("/") 19 | public class HelloController { 20 | 21 | @Autowired 22 | private StudentService studentService; 23 | 24 | @Autowired 25 | private TenantInfoDao tenantInfoDao; 26 | 27 | @RequestMapping 28 | public Result hello() { 29 | List tenantInfoList = tenantInfoDao.findAll(); 30 | for (TenantInfo info : tenantInfoList) { 31 | TenantDataSourceProvider.addDataSource(info); 32 | } 33 | return ResultGenerator.genSuccessResult(tenantInfoList); 34 | } 35 | 36 | @RequestMapping("login") 37 | public Result login(@Param("t") String t) { 38 | ConstId.Id = t; 39 | return ResultGenerator.genSuccessResult(); 40 | } 41 | 42 | @RequestMapping("select") 43 | public Result getStudent(@Param("t") String t) { 44 | return ResultGenerator.genSuccessResult(studentService.findAll()); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/cloud/entity/TenantInfo.java: -------------------------------------------------------------------------------- 1 | package cloud.entity; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Id; 6 | import javax.persistence.Table; 7 | 8 | @Entity 9 | @Table(name = "tenant_info") 10 | public class TenantInfo { 11 | 12 | @Id 13 | @Column(name = "id") 14 | private Integer id; 15 | 16 | @Column(name = "tenant_id") 17 | private String tenantId; 18 | 19 | @Column(name = "tenant_type") 20 | private String tenantType; 21 | 22 | @Column(name = "url") 23 | private String url; 24 | 25 | @Column(name = "username") 26 | private String username; 27 | 28 | @Column(name = "password") 29 | private String password; 30 | 31 | public Integer getId() { 32 | return id; 33 | } 34 | 35 | public void setId(Integer id) { 36 | this.id = id; 37 | } 38 | 39 | public String getTenantId() { 40 | return tenantId; 41 | } 42 | 43 | public void setTenantId(String tenantId) { 44 | this.tenantId = tenantId; 45 | } 46 | 47 | public String getTenantType() { 48 | return tenantType; 49 | } 50 | 51 | public void setTenantType(String tenantType) { 52 | this.tenantType = tenantType; 53 | } 54 | 55 | public String getUrl() { 56 | return url; 57 | } 58 | 59 | public void setUrl(String url) { 60 | this.url = url; 61 | } 62 | 63 | public String getUsername() { 64 | return username; 65 | } 66 | 67 | public void setUsername(String username) { 68 | this.username = username; 69 | } 70 | 71 | public String getPassword() { 72 | return password; 73 | } 74 | 75 | public void setPassword(String password) { 76 | this.password = password; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/cloud/tenant/TenantDataSourceProvider.java: -------------------------------------------------------------------------------- 1 | package cloud.tenant; 2 | 3 | import cloud.entity.TenantInfo; 4 | import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; 5 | 6 | import javax.sql.DataSource; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * 这个类负责根据租户ID来提供对应的数据源 12 | * @author lanyuanxiaoyao 13 | * @version 1.0 14 | */ 15 | public class TenantDataSourceProvider { 16 | 17 | // 使用一个map来存储我们租户和对应的数据源,租户和数据源的信息就是从我们的tenant_info表中读出来 18 | private static Map dataSourceMap = new HashMap<>(); 19 | 20 | /** 21 | * 静态建立一个数据源,也就是我们的默认数据源,假如我们的访问信息里面没有指定tenantId,就使用默认数据源。 22 | * 在我这里默认数据源是cloud_config,实际上你可以指向你们的公共信息的库,或者拦截这个操作返回错误。 23 | */ 24 | static { 25 | DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(); 26 | dataSourceBuilder.url("jdbc:postgresql://localhost:5432/cloud_config"); 27 | dataSourceBuilder.username("lanyuanxiaoyao"); 28 | dataSourceBuilder.password(""); 29 | dataSourceBuilder.driverClassName("org.postgresql.Driver"); 30 | dataSourceMap.put("Default", dataSourceBuilder.build()); 31 | } 32 | 33 | // 根据传进来的tenantId决定返回的数据源 34 | public static DataSource getTenantDataSource(String tenantId) { 35 | if (dataSourceMap.containsKey(tenantId)) { 36 | System.out.println("GetDataSource:" + tenantId); 37 | return dataSourceMap.get(tenantId); 38 | } else { 39 | System.out.println("GetDataSource:" + "Default"); 40 | return dataSourceMap.get("Default"); 41 | } 42 | } 43 | 44 | // 初始化的时候用于添加数据源的方法 45 | public static void addDataSource(TenantInfo tenantInfo) { 46 | DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(); 47 | dataSourceBuilder.url(tenantInfo.getUrl()); 48 | dataSourceBuilder.username(tenantInfo.getUsername()); 49 | dataSourceBuilder.password(tenantInfo.getPassword()); 50 | dataSourceBuilder.driverClassName("org.postgresql.Driver"); 51 | dataSourceMap.put(tenantInfo.getTenantId(), dataSourceBuilder.build()); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 多租户 2 | > **多租户(Multi Tenancy/Tenant)** 是一种软件架构,其定义是:**在一台服务器上运行单个应用实例,它为多个租户提供服务**。 3 | 4 | 概念是抽象的,但是理解起来并不困难,简单来说就是分组,举个例子:我们管理学校学生的时候,可以按照不同的范围来进行分组,比如我们可以按照**学生个人**为单位进行分组,也可以按照**班级**为单位进行分组,然后班级下面有很多的学生,也可以按照**年级**为单位进行分组,以**学校**为单位……这样的每一个分组的单位,都可以是我们概念里面说的一个**租户**。 5 | 但是这样不就和我们以前说的按照面向对象来分类是一样的吗?其实是差不多的,但是有着一些细节上的差别,首先多租户架构的概念是**针对数据存储**的,我们是一个**数据服务提供商**,假设我们给所有的学校提供服务,对于我们来说,分组是按照**学校**为单位的,而且学校与学校之间互相没有任何关系,也就说学校与学校之间是**隔离**的,对于不同学校的数据我们需要将它们隔离开来。这种数据的分组就是多租户架构要研究的问题。 6 | 当然这只是概念上的区别,在实际使用上和我们传统的分组并无太大差异。 7 | 8 | ## 多租户的三种模式 9 | 多租户的架构分为以下三种: 10 | 1. 独立数据库 11 | 2. 共享数据库,独立Schema 12 | 3. 共享数据库,独立Schema,共享数据表 13 | 14 | *注:在这个架构的概念里面,数据库指的是物理机器数据库,也就是我们的一部运行着数据库软件的计算机是一个物理数据库,Schema就是我们在数据库软件里面创建的“数据库”,实际上都是在同一个物理机器里面的,表就是表,一个简单的表* 15 | 16 | 独立数据库是一个租户独享一个数据库实例,它提供了**最强的分离度**,租户的数据彼此**物理不可见**,备份与恢复都很**灵活**; 17 | 共享数据库、独立 Schema 将每个租户关联到同一个数据库的不同 Schema,租户间数据彼此逻辑不可见,上层应用程序的实现和独立数据库一样简单,但备份恢复稍显复杂; 18 | 最后一种模式则是租户数据在数据表级别实现共享,它提供了最低的成本,但引入了额外的编程复杂性(程序的数据访问需要用 tenantId 来区分不同租户),备份与恢复也更复杂。 19 | 这三种模式的特点可以用一张图来概括: 20 | 21 | ![三种部署模式的异同](https://www.ibm.com/developerworks/cn/java/j-lo-dataMultitenant/image001.jpg) 22 | 23 | ## 多租户模式选择 24 | 从上面的图我们可以看到,在成本上,独立数据库是最高的,毕竟我们一个租户就是一个物理机器,而且数据共享起来会麻烦,涉及到跨物理机器的通信,但这种模式的优势体现在单个租户数据量庞大,而且有非常大的扩展需求,那么单个机器内的调整就非常容易,而且不会影响到其他的租户,因为它的隔离程度是最高的。 25 | 事实上,多租户模式的选择,主要是成本原因,对于多数场景而言,**共享度越高**,软硬件**资源利用效率更好**,**成本更低**。但同时也要解决好租户资源共享和隔离带来的安全与性能、扩展性等问题。毕竟,也有客户无法满意于将数据与其他租户放在共享资源中。 26 | 27 | # Hibernate 多租户的使用 28 | ## Mybatis 多租户的使用 29 | 一开始我也是使用Mybatis进行多租户的设计,但是事实上Mybatis本身是没有对多租户提供支持的,也就说我们如果使用Mybatis设计多租户的架构的话,那么我们就需要手动实现sql语句的拦截然后在执行具体sql语句之前执行`use tenant_id`的操作,拦截sql语句的一个比较简单的方式是通过spring aop在service层的操作里进行切入实现拦截。 30 | 实际上Hibernate也是这么干的,不过Hibernate在框架层面帮我们进行了sql语句拦截,不需要自己设计。 31 | 虽然最后我选择了Hibernate进行多租户的设计,但是这里也记录下Mybatis的设计思路,实现起来就简单了。 32 | 33 | ## 项目结构 34 | 可能与Github(地址在文章末尾)实际编码有点出入,因为我可能会修改,但大体相同。 35 | 36 | ![][1] 37 | 38 | ### 主要目录及文件说明 39 | - config 40 | 一些设置文件,一开始我有一些设置文件的,但是后来去掉了,所以你可以忽略这个设置文件夹 41 | - ConstId 42 | 用来暂存租户ID`TenantId`的一个文件,没有特别的作用,通常情况下,这个租户ID是登陆的时候存在session里面的,然后读取也是从session里面读取,这里显然是我为了方便就随便用一个文件来存了 43 | - controller 44 | 顾名思义…… 45 | - HelloController 46 | - dao 47 | 这个也不解释了,dao层 48 | - StudentDao 49 | - TenantInfoDao 50 | - entity 51 | 实体类…… 52 | - Student 53 | - TenantInfo 54 | 这个是租户信息的实体类 55 | - service 56 | Service层,只有一个StudentService是因为我嫌麻烦就不多创建一个TenantInfoService了 57 | - StudentService 58 | - tenant 59 | 多租户相关的文件都在这里了,这个文件夹下的文件是**重点**!这些类的作用会在下面详细分析,这里就先不赘述了 60 | - MultiTenantConnectionProviderImpl 61 | - MultiTenantIdentifierResolver 62 | - TenantDataSourceProvider 63 | - util 64 | 一些辅助的工具,方便操作用的(各个web项目都可以通用,大家可以参考) 65 | - JsonUtil 66 | 给Gson整了一个单例,不同到处new Gson() 67 | - Result 68 | 统一的返回结果格式,满足REST架构 69 | - ResultCode 70 | 统一的返回码,参照HTTP响应码 71 | - ResultGenerator 72 | 构造返回Result结果的工具类 73 | - CloudApplication.java 74 | 75 | ### 数据库结构和说明 76 | 首先在数据库里有三个Schema,其中`cloud_config`是存储租户信息的,`class_1`和`class_2`分别为我们预设的两个租户 77 | 78 | ![][2] 79 | 80 | #### `cloud_config`的`tenant_info`表结构 81 | ![][3] 82 | 83 | ![][4] 84 | 85 | - 字段说明 86 | - id 87 | 主键 88 | - tenant_type 89 | 数据库类型,用于识别连接不同的数据库的时候设置驱动的字段,在我这个小Demo中没有用上 90 | - url 91 | 数据库连接URL 92 | - username 93 | 数据库连接用户名 94 | - password 95 | 数据库连接密码 96 | - tenant_id 97 | 租户ID 98 | 99 | #### `class_1`和`class_2`的`student`表结构 100 | ![][5] 101 | 102 | ![][6] 103 | 104 | ![][7] 105 | 106 | ## 代码 107 | 实际上需要设置的代码非常简单,但是网上的资料极其稀少,很多Demo项目都没有注释和说明,让我走了很多弯路,也是促使我写一个博客来说明这个多租户配置和使用的主要动力 108 | 109 | ### application.properties 110 | 怎么配置开启Hibernate的多租户功能,网上各种配置形式都有,有两种形式,一种是写配置类,一种就是在`application.properties`文件直接配置,显然直接配置要比配置类简单太多了 111 | ``` 112 | # Database 113 | spring.datasource.url=jdbc:postgresql://localhost:5432/cloud_config 114 | spring.datasource.username=lanyuanxiaoyao 115 | spring.datasource.password= 116 | spring.datasource.driver-class-name=org.postgresql.Driver 117 | 118 | # Hibernate 119 | spring.jpa.show-sql=true 120 | spring.jpa.properties.hibernate.multiTenancy=SCHEMA 121 | spring.jpa.properties.hibernate.tenant_identifier_resolver=cloud.tenant.MultiTenantIdentifierResolver 122 | spring.jpa.properties.hibernate.multi_tenant_connection_provider=cloud.tenant.MultiTenantConnectionProviderImpl 123 | ``` 124 | 这就是所需要的所有相关配置(如果你有别的配置就另外加上就是了),其中Database配置一定要有,就是一定要有一个默认的配置才能启动Spring boot,这个不能省……这是一个坑。 125 | - 关于Hibernate的几个配置项的说明 126 | - **show-sql** 127 | 这个也无关多租户的设置,只是在控制台显示Hibernate执行的sql语句,方便调试 128 | - **hibernate.multiTenancy** 129 | 选择多租户的模式,有四个参数:`NONE`,`DATABASE`,`SCHEMA`,`DISCRIMINATOR`,其中`NONE`就是默认没有模式,`DISCRIMINATOR`会在Hibernate5支持,所以我们根据模式选择是独立数据库还是不独立数据库就可以了,我这里选择SCHEMA,因为只有一台物理机器 130 | - **hibernate.tenant_identifier_resolver** 131 | 租户ID解析器,简单来说就是这个设置指定的类负责每次执行sql语句的时候获取租户ID 132 | - **hibernate.multi_tenant_connection_provider** 133 | 这个设置指定的类负责按照租户ID来提供相应的数据源 134 | 135 | **配置后三个设置项的时候会没有自动提示,直接复制就行了,只要名字没错就ok,因为没有自动提示搞到我以为设置在这里是不行的** 136 | 137 | ### tenant包 138 | 这里的三个类是全部和多租户相关的类,这里我连同导包的信息也一并贴上了,希望大家不要导错包,同名的包有不少 139 | #### TenantDataSourceProvider 140 | ```java 141 | import cloud.entity.TenantInfo; 142 | import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; 143 | 144 | import javax.sql.DataSource; 145 | import java.util.HashMap; 146 | import java.util.Map; 147 | 148 | /** 149 | * @author lanyuanxiaoyao 150 | */ 151 | public class TenantDataSourceProvider { 152 | 153 | // 使用一个map来存储我们租户和对应的数据源,租户和数据源的信息就是从我们的tenant_info表中读出来 154 | private static Map dataSourceMap = new HashMap<>(); 155 | 156 | /** 157 | * 静态建立一个数据源,也就是我们的默认数据源,假如我们的访问信息里面没有指定tenantId,就使用默认数据源。 158 | * 在我这里默认数据源是cloud_config,实际上你可以指向你们的公共信息的库,或者拦截这个操作返回错误。 159 | */ 160 | static { 161 | DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(); 162 | dataSourceBuilder.url("jdbc:postgresql://localhost:5432/cloud_config"); 163 | dataSourceBuilder.username("lanyuanxiaoyao"); 164 | dataSourceBuilder.password(""); 165 | dataSourceBuilder.driverClassName("org.postgresql.Driver"); 166 | dataSourceMap.put("Default", dataSourceBuilder.build()); 167 | } 168 | 169 | // 根据传进来的tenantId决定返回的数据源 170 | public static DataSource getTenantDataSource(String tenantId) { 171 | if (dataSourceMap.containsKey(tenantId)) { 172 | System.out.println("GetDataSource:" + tenantId); 173 | return dataSourceMap.get(tenantId); 174 | } else { 175 | System.out.println("GetDataSource:" + "Default"); 176 | return dataSourceMap.get("Default"); 177 | } 178 | } 179 | 180 | // 初始化的时候用于添加数据源的方法 181 | public static void addDataSource(TenantInfo tenantInfo) { 182 | DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(); 183 | dataSourceBuilder.url(tenantInfo.getUrl()); 184 | dataSourceBuilder.username(tenantInfo.getUsername()); 185 | dataSourceBuilder.password(tenantInfo.getPassword()); 186 | dataSourceBuilder.driverClassName("org.postgresql.Driver"); 187 | dataSourceMap.put(tenantInfo.getTenantId(), dataSourceBuilder.build()); 188 | } 189 | 190 | } 191 | ``` 192 | #### MultiTenantConnectionProviderImpl 193 | ```java 194 | import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl; 195 | import javax.sql.DataSource; 196 | 197 | /** 198 | * 这个类是Hibernate框架拦截sql语句并在执行sql语句之前更换数据源提供的类 199 | * @author lanyuanxiaoyao 200 | * @version 1.0 201 | */ 202 | public class MultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl { 203 | 204 | // 在没有提供tenantId的情况下返回默认数据源 205 | @Override 206 | protected DataSource selectAnyDataSource() { 207 | return TenantDataSourceProvider.getTenantDataSource("Default"); 208 | } 209 | 210 | // 提供了tenantId的话就根据ID来返回数据源 211 | @Override 212 | protected DataSource selectDataSource(String tenantIdentifier) { 213 | return TenantDataSourceProvider.getTenantDataSource(tenantIdentifier); 214 | } 215 | } 216 | ``` 217 | #### MultiTenantIdentifierResolver 218 | ```java 219 | package cloud.tenant; 220 | 221 | import cloud.config.ConstId; 222 | import org.hibernate.context.spi.CurrentTenantIdentifierResolver; 223 | 224 | /** 225 | * 这个类是由Hibernate提供的用于识别tenantId的类,当每次执行sql语句被拦截就会调用这个类中的方法来获取tenantId 226 | * @author lanyuanxiaoyao 227 | * @version 1.0 228 | */ 229 | public class MultiTenantIdentifierResolver implements CurrentTenantIdentifierResolver{ 230 | 231 | // 获取tenantId的逻辑在这个方法里面写 232 | @Override 233 | public String resolveCurrentTenantIdentifier() { 234 | if (!"".equals(ConstId.Id)){ 235 | return ConstId.Id; 236 | } 237 | return "Default"; 238 | } 239 | 240 | @Override 241 | public boolean validateExistingCurrentSessions() { 242 | return true; 243 | } 244 | } 245 | ``` 246 | ## Hibernate 多租户实现原理 247 | 真如前面所说,Hibernate实现多租户的原理实际上就是在调用具体sql语句之前先调用一句`user database`来切换数据库,实现切换租户空间的功能,所以Hibernate提供了两个类来帮助我们在框架层面拦截我们要执行的sql语句,并注入切换数据库的操作,操作流程见下图: 248 | 249 | ![][8] 250 | 251 | ## 测试 252 | 因为Demo实在是简单,所以有一些细节没有处理,包括从session中取tenantId也没有写进去,所以测试流程就先写下来,免得无法测试实际项目效果 253 | 254 | ### 初始化`datasourceMap` 255 | 访问`http://localhost:8080/` 256 | 257 | ![][9] 258 | 259 | 可以看到我们从`cloud_config`schema的`tenant_info`获取到所有租户的信息 260 | 261 | ### 登陆 262 | 访问`http://localhost:8080/login?t=class_1` 263 | 264 | ![][10] 265 | 266 | 看到返回成功,即后台已经设置好了`tenantId`为`class_1` 267 | 268 | ### 查询 269 | 访问`http://localhost:8080/select?t=class_1` 270 | 271 | ![][11] 272 | 273 | 可以看到我们在不改动实际数据库连接的情况下获取到了`class_1`schema的`student`的数据,到这里我们已经可以访问租户的信息了 274 | 275 | ### 切换租户 276 | 我们重复登录和查询的步骤 277 | 访问`http://localhost:8080/login?t=class_2`和`http://localhost:8080/select?t=class_2` 278 | 279 | ![][12] 280 | 281 | 我们成功获取到了另一个租户的信息,到这里我们多租户的实现已经成功了。 282 | 283 | # 总结 284 | 多租户架构这个看起来好像还挺新的,也许是应用范围不够广泛,网上的资料相当少,也让我走了很多的弯路,在此总结这篇文档,希望能够帮助到大家。 285 | Demo的GIthub地址:[https://github.com/lanyuanxiaoyao/multi-tenant](https://github.com/lanyuanxiaoyao/multi-tenant) 286 | 287 | 288 | [1]: https://www.github.com/lanyuanxiaoyao/GitGallery/raw/master/2017/7/14/Spring%20Boot%EF%BC%88%E4%B8%89%EF%BC%89%20Spring%20boot%20+%20Hibernate%20%E5%A4%9A%E7%A7%9F%E6%88%B7%E7%9A%84%E4%BD%BF%E7%94%A8/Ashampoo_Snap_2017%E5%B9%B47%E6%9C%8814%E6%97%A5_12h27m50s_001_.png "目录结构" 289 | [2]: https://www.github.com/lanyuanxiaoyao/GitGallery/raw/master/2017/7/14/Spring%20Boot%EF%BC%88%E4%B8%89%EF%BC%89%20Spring%20boot%20+%20Hibernate%20%E5%A4%9A%E7%A7%9F%E6%88%B7%E7%9A%84%E4%BD%BF%E7%94%A8/Ashampoo_Snap_2017%E5%B9%B47%E6%9C%8814%E6%97%A5_13h40m46s_002_.png "数据库结构" 290 | [3]: https://www.github.com/lanyuanxiaoyao/GitGallery/raw/master/2017/7/14/Spring%20Boot%EF%BC%88%E4%B8%89%EF%BC%89%20Spring%20boot%20+%20Hibernate%20%E5%A4%9A%E7%A7%9F%E6%88%B7%E7%9A%84%E4%BD%BF%E7%94%A8/Ashampoo_Snap_2017%E5%B9%B47%E6%9C%8814%E6%97%A5_13h41m52s_005_.png "cloud_config" 291 | [4]: https://www.github.com/lanyuanxiaoyao/GitGallery/raw/master/2017/7/14/Spring%20Boot%EF%BC%88%E4%B8%89%EF%BC%89%20Spring%20boot%20+%20Hibernate%20%E5%A4%9A%E7%A7%9F%E6%88%B7%E7%9A%84%E4%BD%BF%E7%94%A8/Ashampoo_Snap_2017%E5%B9%B47%E6%9C%8814%E6%97%A5_13h46m34s_006_.png "cloud_config表结构" 292 | [5]: https://www.github.com/lanyuanxiaoyao/GitGallery/raw/master/2017/7/14/Spring%20Boot%EF%BC%88%E4%B8%89%EF%BC%89%20Spring%20boot%20+%20Hibernate%20%E5%A4%9A%E7%A7%9F%E6%88%B7%E7%9A%84%E4%BD%BF%E7%94%A8/Ashampoo_Snap_2017%E5%B9%B47%E6%9C%8814%E6%97%A5_13h41m27s_003_.png "class_1" 293 | [6]: https://www.github.com/lanyuanxiaoyao/GitGallery/raw/master/2017/7/14/Spring%20Boot%EF%BC%88%E4%B8%89%EF%BC%89%20Spring%20boot%20+%20Hibernate%20%E5%A4%9A%E7%A7%9F%E6%88%B7%E7%9A%84%E4%BD%BF%E7%94%A8/Ashampoo_Snap_2017%E5%B9%B47%E6%9C%8814%E6%97%A5_13h41m39s_004_.png "class_2" 294 | [7]: https://www.github.com/lanyuanxiaoyao/GitGallery/raw/master/2017/7/14/Spring%20Boot%EF%BC%88%E4%B8%89%EF%BC%89%20Spring%20boot%20+%20Hibernate%20%E5%A4%9A%E7%A7%9F%E6%88%B7%E7%9A%84%E4%BD%BF%E7%94%A8/Ashampoo_Snap_2017%E5%B9%B47%E6%9C%8814%E6%97%A5_13h58m44s_007_.png "student表结构" 295 | [8]: https://www.github.com/lanyuanxiaoyao/GitGallery/raw/master/2017/7/14/Spring%20Boot%EF%BC%88%E4%B8%89%EF%BC%89%20Spring%20boot%20+%20Hibernate%20%E5%A4%9A%E7%A7%9F%E6%88%B7%E7%9A%84%E4%BD%BF%E7%94%A8/multi-tenant.png "multi-tenant" 296 | [9]: https://www.github.com/lanyuanxiaoyao/GitGallery/raw/master/2017/7/14/Spring%20Boot%EF%BC%88%E4%B8%89%EF%BC%89%20Spring%20boot%20+%20Hibernate%20%E5%A4%9A%E7%A7%9F%E6%88%B7%E7%9A%84%E4%BD%BF%E7%94%A8/Ashampoo_Snap_2017%E5%B9%B47%E6%9C%8814%E6%97%A5_18h40m27s_001_.png "初始化datasourceMap" 297 | [10]: https://www.github.com/lanyuanxiaoyao/GitGallery/raw/master/2017/7/14/Spring%20Boot%EF%BC%88%E4%B8%89%EF%BC%89%20Spring%20boot%20+%20Hibernate%20%E5%A4%9A%E7%A7%9F%E6%88%B7%E7%9A%84%E4%BD%BF%E7%94%A8/Ashampoo_Snap_2017%E5%B9%B47%E6%9C%8814%E6%97%A5_18h43m21s_002_.png "登陆" 298 | [11]: https://www.github.com/lanyuanxiaoyao/GitGallery/raw/master/2017/7/14/Spring%20Boot%EF%BC%88%E4%B8%89%EF%BC%89%20Spring%20boot%20+%20Hibernate%20%E5%A4%9A%E7%A7%9F%E6%88%B7%E7%9A%84%E4%BD%BF%E7%94%A8/Ashampoo_Snap_2017%E5%B9%B47%E6%9C%8814%E6%97%A5_18h45m28s_003_.png "查询" 299 | [12]: https://www.github.com/lanyuanxiaoyao/GitGallery/raw/master/2017/7/14/Spring%20Boot%EF%BC%88%E4%B8%89%EF%BC%89%20Spring%20boot%20+%20Hibernate%20%E5%A4%9A%E7%A7%9F%E6%88%B7%E7%9A%84%E4%BD%BF%E7%94%A8/Ashampoo_Snap_2017%E5%B9%B47%E6%9C%8814%E6%97%A5_18h51m40s_004_.png "查询" 300 | --------------------------------------------------------------------------------