spring+hibernate多租户(一)

一、事例说明

hibernate多租户官方文档

  1. 本示例中约定域名等于数据库名

    例如:访问域名http://www.domain.com/,则数据库名为www.domain.com。

  2. 本示例代码:代码地址

    maven构建,具体配置请查看示例代码
    spring.version 5.0.2.RELEASE
    hibernate.version 5.2.12.Final
    非分布式
    数据库为单机mysql(先创建默认库,如www.domain.com)

  3. 主要流程

    1. Spring拦截器拦截请求域名例如:http://www.domain.com/
    2. ThreadLocal中保存域名www.domain.com(用户标识符)
    3. Hibernate获取ThreadLocal中保存的域名www.domain.com(用户标识符)
    4. Hibernate根据当前域名(用户标识符)请求对应的数据源

二、主要配置

  1. 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>
  1. 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>
  1. 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>
  1. 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代码


  1. 请求拦截器

拦截器获取请求域名,将域名赋值给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;
    }
}
  1. 存储访问域名,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();
    }

}
  1. 数据源连接工具
    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;
    }

}
  1. 编写多租户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;
    }
}

四、 测试

  1. 本示例用域名进行测试,修改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

    hosts文件

  2. 本示例中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>
  1. 示例截图
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述

五、参考文档

hibernate多租户官方文档
Hibernate Multi Tenancy with Spring

本示例代码:代码地址

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值