一、事例说明
本示例中约定域名等于数据库名。
例如:访问域名http://www.domain.com/,则数据库名为www.domain.com。
本示例代码:代码地址
maven构建,具体配置请查看示例代码
spring.version 5.0.2.RELEASE
hibernate.version 5.2.12.Final
非分布式
数据库为单机mysql(先创建默认库,如www.domain.com)主要流程
- Spring拦截器拦截请求域名例如:http://www.domain.com/
- ThreadLocal中保存域名www.domain.com(用户标识符)
- Hibernate获取ThreadLocal中保存的域名www.domain.com(用户标识符)
- Hibernate根据当前域名(用户标识符)请求对应的数据源
二、主要配置
- web.xml 不要开启openSessionInView
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0" metadata-complete="true">
<display-name>Archetype Created Web Application</display-name>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:spring-context.xml,
classpath:spring-mvc.xml
</param-value>
</context-param>
<filter>
<filter-name>Character Encoding</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-mapping>
<filter-name>Character Encoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 不用开启OpenSessionInViewFilter,避免请求直接开启session打开数据源,导致拦截器在连接数据源后执行 -->
<!--
<filter>
<filter-name>hibernateOpenSessionInViewFilter</filter-name>
<filter-class>org.springframework.orm.hibernate5.support.OpenSessionInViewFilter</filter-class>
<init-param>
<param-name>flushMode</param-name>
<param-value>AUTO</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>hibernateOpenSessionInViewFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
-->
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
- spring-context.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" 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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 扫描包 -->
<context:component-scan base-package="com.tongna">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 加载配置文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 多租户 配置hibernate SessionFactory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="packagesToScan" value="com.tongna.domain" />
<property name="hibernateProperties">
<props>
<!-- Hibernate配置 -->
<!--绑定session到当前线程,不用spring事务-->
<!--<prop key="hibernate.current_session_context_class">thread</prop>
<prop key="hibernate.connection.autocommit">true</prop>-->
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.format_sql">true</prop>
<!-- 多租户配置 -->
<prop key="hibernate.multiTenancy">SCHEMA</prop>
<!-- 属性规定了一个合约,以使 Hibernate 能够解析出应用当前的 tenantId,-->
<!-- 该类必须实现 CurrentTenantIdentifierResolver 接口 -->
<prop key="hibernate.tenant_identifier_resolver">com.tongna.tenant.CurrentTenantIdentifierResolverImpl</prop>
<!-- 指定了 ConnectionProvider,即 Hibernate 需要知道如何以租户特有的方式获取数据连接 -->
<prop key="hibernate.multi_tenant_connection_provider">com.tongna.tenant.MultiTenantConnectionProviderImpl</prop>
</props>
</property>
</bean>
<!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="autodetectDataSource" value="false"/>
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<!-- 启动注解用注解来管理事务 -->
<tx:annotation-driven/>
</beans>
- spring-mvc.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:mvc="http://www.springframework.org/schema/mvc"
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/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<mvc:annotation-driven>
<mvc:message-converters>
<ref bean="jacksonConverters"/>
</mvc:message-converters>
</mvc:annotation-driven>
<context:component-scan base-package="com.tongna.controller"/>
<!-- freemarker config -->
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
<property name="defaultEncoding" value="utf-8"/>
<property name="freemarkerVariables">
<map>
<entry key="rootPath" value="#{servletContext.contextPath}"/>
</map>
</property>
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
<property name="contentType" value="text/html;charset=UTF-8"/>
<property name="cache" value="false"/>
<property name="prefix" value=""/>
<property name="suffix" value=".ftl"/>
</bean>
<!-- 拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.tongna.TenantInterceptor"/>
</mvc:interceptor>
<ref bean="openSessionInViewInterceptor"/>
</mvc:interceptors>
<bean name="openSessionInViewInterceptor" class="org.springframework.orm.hibernate5.support.OpenSessionInViewInterceptor">
<property name="sessionFactory">
<ref bean="sessionFactory"/>
</property>
</bean>
<bean id="jacksonConverters" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper" ref="objectMapper"/>
</bean>
<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
<property name="indentOutput" value="true"/>
<property name="simpleDateFormat" value="yyyy-MM-dd"/>
</bean>
</beans>
- jdbc.properties配置文件,主要用在DBUtils.java中
#url前缀、后缀、默认数据库
jdbc.url.prefix=jdbc:mysql://192.168.0.199:3306/
jdbc.url.suffix=?characterEncoding=UTF-8&useSSL=false
jdbc.databaseName=www.domain.com
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.0.199:3306/www.domain.com?characterEncoding=UTF-8&useSSL=false
jdbc.username=xxx
jdbc.password=xxx
三、 主要java代码
- 请求拦截器
拦截器获取请求域名,将域名赋值给ThreadLocal,供hibernate中调用
TenantInterceptor.java
package com.tongna;
import com.tongna.tenant.CurrentTenantIdentifierHolder;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* TenantDemo
*
* @author 张浩伟
* @version 1.01 2018年02月09日
*/
public class TenantInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取域名
String serverName = request.getServerName();
if (serverName != null) {
CurrentTenantIdentifierHolder.set(serverName);
} else {
throw new RuntimeException("未找到主机名");
}
// 简单权限
request.getSession().setAttribute("admin", "www.domain.com".equals(serverName));
request.getSession().setAttribute("domain", serverName);
return true;
}
}
- 存储访问域名,ThreadLocal
CurrentTenantIdentifierHolder.java
package com.tongna.tenant;
/**
* TenantDemo
*
* @author 张浩伟
* @version 1.01 2018年02月09日
*/
public class CurrentTenantIdentifierHolder {
private static final ThreadLocal<String> ctx = new ThreadLocal<String>();
public static void set(String tenantIdentifier) {
ctx.set(tenantIdentifier);
}
public static String get() {
return ctx.get();
}
public static void clear() {
ctx.remove();
}
}
- 数据源连接工具
DBUtils.java
package com.tongna.util;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.mchange.v2.c3p0.DataSources;
import jodd.io.FileUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import java.beans.PropertyVetoException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
/**
* usite
*
* @author 张浩伟
* @version 1.01 2018年02月06日
*/
public class DBUtils {
private static Logger log = LoggerFactory.getLogger(DBUtils.class);
public static ComboPooledDataSource createDateSource(String databasesName, String username, String password){
ComboPooledDataSource defaultDataSource = new ComboPooledDataSource(databasesName);
Properties properties = loadProperties();
if (StringUtils.isNotBlank(databasesName)){
defaultDataSource.setDataSourceName(databasesName);
defaultDataSource.setJdbcUrl(properties.getProperty("jdbc.url.prefix")
+ databasesName + properties.getProperty("jdbc.url.suffix"));
} else {
defaultDataSource.setDataSourceName(properties.getProperty("jdbc.databaseName"));
defaultDataSource.setJdbcUrl(properties.getProperty("jdbc.url"));
}
if (StringUtils.isNotBlank(username)){
defaultDataSource.setUser(username);
} else {
defaultDataSource.setUser(properties.getProperty("jdbc.username"));
}
if (StringUtils.isNotBlank(password)){
defaultDataSource.setPassword(password);
} else {
defaultDataSource.setPassword(properties.getProperty("jdbc.password"));
}
defaultDataSource.setInitialPoolSize(16);
defaultDataSource.setMaxConnectionAge(10000);
try {
defaultDataSource.setDriverClass(properties.getProperty("jdbc.driverClassName"));
} catch (PropertyVetoException e) {
e.printStackTrace();
}
log.info("数据库初始化信息:url={}", defaultDataSource.getJdbcUrl());
log.info(" :username={}", defaultDataSource.getUser());
log.info(" :password={}", defaultDataSource.getPassword());
log.info(" :driverClass={}", defaultDataSource.getDriverClass());
return defaultDataSource;
}
public static Properties loadProperties(){
try {
return PropertiesLoaderUtils.loadAllProperties("jdbc.properties");
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static boolean createDataBases(String databasesName) {
try {
ComboPooledDataSource dateSource = createDateSource(null, null, null);
JdbcTemplate template = getTemplate(dateSource);
String sql = "CREATE DATABASE IF NOT EXISTS `"+databasesName+"` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;";
template.execute(sql);
ComboPooledDataSource newDateSource = createDateSource(databasesName, null, null);
template.setDataSource(newDateSource);
template.execute("use `" + databasesName +"`");
template.batchUpdate(loadSql());
log.info("创建数据库[{}]成功", databasesName);
return true;
} catch (DataAccessException e) {
e.printStackTrace();
}
return false;
}
private static String[] loadSql(){
String[] result = new String[0];
try {
Resource resource = new ClassPathResource("news.sql");
File resourceFile = resource.getFile();
if (resourceFile.canRead()){
String[] lines = FileUtil.readLines(resourceFile, "UTF-8");
List<String> sqls = Arrays.asList(lines);
List<String> tmp = new ArrayList<String>();
StringBuilder sb = new StringBuilder();
for (String sql : sqls) {
sb.append(sql);
if (sb.indexOf(";") != -1){
tmp.add(sb.toString());
sb.delete(0, sb.length());
}
}
return tmp.toArray(result);
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
private static JdbcTemplate getTemplate(ComboPooledDataSource dataSource) {
JdbcTemplate template = new JdbcTemplate();
template.setDataSource(dataSource);
return template;
}
}
- 编写多租户hibernate获取用户标识及获取数据源实现类
CurrentTenantIdentifierResolverImpl.java
package com.tongna.tenant;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TenantDemo
* 属性规定了一个合约,以使 Hibernate 能够解析出应用当前的 tenantId
*
* @author 张浩伟
* @version 1.01 2018年02月09日
*/
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {
private static Logger log = LoggerFactory.getLogger(CurrentTenantIdentifierResolverImpl.class);
/**
* @return
* 应用当前的 tenantId
*/
public String resolveCurrentTenantIdentifier() {
return CurrentTenantIdentifierHolder.get();
}
public boolean validateExistingCurrentSessions() {
return true;
}
}
MultiTenantConnectionProviderImpl.java
package com.tongna.tenant;
import com.mchange.v2.c3p0.C3P0Registry;
import com.mchange.v2.c3p0.PooledDataSource;
import com.tongna.util.DBUtils;
import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl;
import javax.sql.DataSource;
/**
* TenantDemo
* 指定了 ConnectionProvider,即 Hibernate 需要知道如何以租户特有的方式获取数据连接
*
* @author 张浩伟
* @version 1.01 2018年02月09日
*/
public class MultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {
/**
* 返回默认的数据源
*/
protected DataSource selectAnyDataSource() {
return DBUtils.createDateSource(null,null,null);
}
/**
* 根据tenantIdentifier返回指定数据源
* @param tenantIdentifier
* @return
*/
protected DataSource selectDataSource(String tenantIdentifier) {
PooledDataSource dataSource = C3P0Registry.pooledDataSourceByName(tenantIdentifier);
if (dataSource == null) {
return DBUtils.createDateSource(null,null,null);
}
return dataSource;
}
}
四、 测试
本示例用域名进行测试,修改windows系统中的hosts文件
文件路径:C:\Windows\System32\drivers\etc\hosts
hosts文件中增加如下代码:127.0.0.1 www.domain.com 127.0.0.1 xa.domain.com 127.0.0.1 bj.domain.com
- 本示例中tomcat端口在8080,利用Nginx将域名反向代理到tomcat中。也可修改tomcat端口为80,不用Nginx代理。
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<charset>UTF-8</charset>
<path>/</path>
<!-- 修改Tomcat端口为80 -->
<port>80</port>
</configuration>
</plugin>
- 示例截图