时间:2023-03-14 11:46:01 | 来源:电子商务
时间:2023-03-14 11:46:01 来源:电子商务
本次教程所涉及到的源码已上传至Github,如果你不需要继续阅读下面的内容,你可以直接点击此链接获取源码内容。https://github.com/ramostear/una-saas-toturial
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> </dependencies>
然后提供一个可用的配置文件,并加入如下的内容:spring: freemarker: cache: false template-loader-path: - classpath:/templates/ prefix: suffix: .html resources: static-locations: - classpath:/static/ devtools: restart: enabled: true jpa: database: mysql show-sql: true generate-ddl: false hibernate: ddl-auto: noneuna: master: datasource: url: jdbc:mysql://localhost:3306/master_tenant?useSSL=false username: root password: root driverClassName: com.mysql.jdbc.Driver maxPoolSize: 10 idleTimeout: 300000 minIdle: 10 poolName: master-database-connection-poollogging: level: root: warn org: springframework: web: debug hibernate: debug
由于采用Freemarker作为视图渲染引擎,所以需要提供Freemarker的相关技术接下来,我们需要关闭Spring Boot自动配置数据源的功能,在项目主类上添加如下的设置:
una:master:datasource配置项就是上面说的统一存放租户信息的数据源配置信息,你可以理解为主库。
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})public class UnaSaasApplication { public static void main(String[] args) { SpringApplication.run(UnaSaasApplication.class, args); }}
最后,让我们看看整个项目的结构:@Data@Entity@Table(name = "MASTER_TENANT")@NoArgsConstructor@AllArgsConstructor@Builderpublic class MasterTenant implements Serializable{ @Id @Column(name="ID") private String id; @Column(name = "TENANT") @NotEmpty(message = "Tenant identifier must be provided") private String tenant; @Column(name = "URL") @Size(max = 256) @NotEmpty(message = "Tenant jdbc url must be provided") private String url; @Column(name = "USERNAME") @Size(min = 4,max = 30,message = "db username length must between 4 and 30") @NotEmpty(message = "Tenant db username must be provided") private String username; @Column(name = "PASSWORD") @Size(min = 4,max = 30) @NotEmpty(message = "Tenant db password must be provided") private String password; @Version private int version = 0;}
持久层我们将继承JpaRepository接口,快速实现对数据源的CURD操作,同时提供了一个通过租户名查找租户数据源的接口,其代码如下:package com.ramostear.una.saas.master.repository;import com.ramostear.una.saas.master.model.MasterTenant;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.data.jpa.repository.Query;import org.springframework.data.repository.query.Param;import org.springframework.stereotype.Repository;/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/25 0025-8:22 * @modify by : * @since: */@Repositorypublic interface MasterTenantRepository extends JpaRepository<MasterTenant,String>{ @Query("select p from MasterTenant p where p.tenant = :tenant") MasterTenant findByTenant(@Param("tenant") String tenant);}
业务层提供通过租户名获取租户数据源信息的服务(其余的服务各位可自行添加):package com.ramostear.una.saas.master.service;import com.ramostear.una.saas.master.model.MasterTenant;/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/25 0025-8:26 * @modify by : * @since: */public interface MasterTenantService { /** * Using custom tenant name query * @param tenant tenant name * @return masterTenant */ MasterTenant findByTenant(String tenant);}
最后,我们需要关注的重点是配置主数据源(Spring Boot需要为其提供一个默认的数据源)。在配置之前,我们需要获取配置项,可以通过@ConfigurationProperties("una.master.datasource")获取配置文件中的相关配置信息:@Getter@Setter@Configuration@ConfigurationProperties("una.master.datasource")public class MasterDatabaseProperties { private String url; private String password; private String username; private String driverClassName; private long connectionTimeout; private int maxPoolSize; private long idleTimeout; private int minIdle; private String poolName; @Override public String toString(){ StringBuilder builder = new StringBuilder(); builder.append("MasterDatabaseProperties [ url=") .append(url) .append(", username=") .append(username) .append(", password=") .append(password) .append(", driverClassName=") .append(driverClassName) .append(", connectionTimeout=") .append(connectionTimeout) .append(", maxPoolSize=") .append(maxPoolSize) .append(", idleTimeout=") .append(idleTimeout) .append(", minIdle=") .append(minIdle) .append(", poolName=") .append(poolName) .append("]"); return builder.toString(); }}
接下来是配置自定义的数据源,其源码如下:package com.ramostear.una.saas.master.config;import com.ramostear.una.saas.master.config.properties.MasterDatabaseProperties;import com.ramostear.una.saas.master.model.MasterTenant;import com.ramostear.una.saas.master.repository.MasterTenantRepository;import com.zaxxer.hikari.HikariDataSource;import lombok.extern.slf4j.Slf4j;import org.hibernate.cfg.Environment;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;import org.springframework.data.jpa.repository.config.EnableJpaRepositories;import org.springframework.orm.jpa.JpaTransactionManager;import org.springframework.orm.jpa.JpaVendorAdapter;import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.persistence.EntityManagerFactory;import javax.sql.DataSource;import java.util.Properties;/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/25 0025-8:31 * @modify by : * @since: */@Configuration@EnableTransactionManagement@EnableJpaRepositories(basePackages = {"com.ramostear.una.saas.master.model","com.ramostear.una.saas.master.repository"}, entityManagerFactoryRef = "masterEntityManagerFactory", transactionManagerRef = "masterTransactionManager")@Slf4jpublic class MasterDatabaseConfig { @Autowired private MasterDatabaseProperties masterDatabaseProperties; @Bean(name = "masterDatasource") public DataSource masterDatasource(){ log.info("Setting up masterDatasource with :{}",masterDatabaseProperties.toString()); HikariDataSource datasource = new HikariDataSource(); datasource.setUsername(masterDatabaseProperties.getUsername()); datasource.setPassword(masterDatabaseProperties.getPassword()); datasource.setJdbcUrl(masterDatabaseProperties.getUrl()); datasource.setDriverClassName(masterDatabaseProperties.getDriverClassName()); datasource.setPoolName(masterDatabaseProperties.getPoolName()); datasource.setMaximumPoolSize(masterDatabaseProperties.getMaxPoolSize()); datasource.setMinimumIdle(masterDatabaseProperties.getMinIdle()); datasource.setConnectionTimeout(masterDatabaseProperties.getConnectionTimeout()); datasource.setIdleTimeout(masterDatabaseProperties.getIdleTimeout()); log.info("Setup of masterDatasource successfully."); return datasource; } @Primary @Bean(name = "masterEntityManagerFactory") public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory(){ LocalContainerEntityManagerFactoryBean lb = new LocalContainerEntityManagerFactoryBean(); lb.setDataSource(masterDatasource()); lb.setPackagesToScan( new String[]{MasterTenant.class.getPackage().getName(), MasterTenantRepository.class.getPackage().getName()} ); //Setting a name for the persistence unit as Spring sets it as 'default' if not defined. lb.setPersistenceUnitName("master-database-persistence-unit"); //Setting Hibernate as the JPA provider. JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); lb.setJpaVendorAdapter(vendorAdapter); //Setting the hibernate properties lb.setJpaProperties(hibernateProperties()); log.info("Setup of masterEntityManagerFactory successfully."); return lb; } @Bean(name = "masterTransactionManager") public JpaTransactionManager masterTransactionManager(@Qualifier("masterEntityManagerFactory")EntityManagerFactory emf){ JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(emf); log.info("Setup of masterTransactionManager successfully."); return transactionManager; } @Bean public PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor(){ return new PersistenceExceptionTranslationPostProcessor(); } private Properties hibernateProperties(){ Properties properties = new Properties(); properties.put(Environment.DIALECT,"org.hibernate.dialect.MySQL5Dialect"); properties.put(Environment.SHOW_SQL,true); properties.put(Environment.FORMAT_SQL,true); properties.put(Environment.HBM2DDL_AUTO,"update"); return properties; }}
在改配置类中,我们主要提供包扫描路径,实体管理工程,事务管理器和数据源配置参数的配置。@Entity@Table(name = "USER")@Data@NoArgsConstructor@AllArgsConstructor@Builderpublic class User implements Serializable { private static final long serialVersionUID = -156890917814957041L; @Id @Column(name = "ID") private String id; @Column(name = "USERNAME") private String username; @Column(name = "PASSWORD") @Size(min = 6,max = 22,message = "User password must be provided and length between 6 and 22.") private String password; @Column(name = "TENANT") private String tenant;}
业务层提供了一个根据用户名检索用户信息的服务,它将调用持久层的方法根据用户名对租户的用户表进行检索,如果找到满足条件的用户记录,则返回用户信息,如果没有找到,则返回null;持久层和业务层的源码分别如下:@Repositorypublic interface UserRepository extends JpaRepository<User,String>,JpaSpecificationExecutor<User>{ User findByUsername(String username);}@Service("userService")public class UserServiceImpl implements UserService{ @Autowired private UserRepository userRepository; private static TwitterIdentifier identifier = new TwitterIdentifier(); @Override public void save(User user) { user.setId(identifier.generalIdentifier()); user.setTenant(TenantContextHolder.getTenant()); userRepository.save(user); } @Override public User findById(String userId) { Optional<User> optional = userRepository.findById(userId); if(optional.isPresent()){ return optional.get(); }else{ return null; } } @Override public User findByUsername(String username) { System.out.println(TenantContextHolder.getTenant()); return userRepository.findByUsername(username); }
在这里,我们采用了Twitter的雪花算法来实现了一个ID生成器。
/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/26 0026-23:17 * @modify by : * @since: */@Slf4jpublic class TenantInterceptor implements HandlerInterceptor{ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String tenant = request.getParameter("tenant"); if(StringUtils.isBlank(tenant)){ response.sendRedirect("/login.html"); return false; }else{ TenantContextHolder.setTenant(tenant); return true; } }}@Configurationpublic class InterceptorConfig extends WebMvcConfigurationSupport { @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new TenantInterceptor()).addPathPatterns("/**").excludePathPatterns("/login.html"); super.addInterceptors(registry); }}
/login.html是系统的登录路径,我们需要将其排除在拦截器拦截的范围之外,否则我们永远无法进行登录
public class TenantContextHolder { private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>(); public static void setTenant(String tenant){ CONTEXT.set(tenant); } public static String getTenant(){ return CONTEXT.get(); } public static void clear(){ CONTEXT.remove(); }}
此类时实现动态数据源设置的关键
package com.ramostear.una.saas.tenant.config;import com.ramostear.una.saas.context.TenantContextHolder;import org.apache.commons.lang3.StringUtils;import org.hibernate.context.spi.CurrentTenantIdentifierResolver;/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/26 0026-22:38 * @modify by : * @since: */public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver { /** * 默认的租户ID */ private static final String DEFAULT_TENANT = "tenant_1"; /** * 解析当前租户的ID * @return */ @Override public String resolveCurrentTenantIdentifier() { //通过租户上下文获取租户ID,此ID是用户登录时在header中进行设置的 String tenant = TenantContextHolder.getTenant(); //如果上下文中没有找到该租户ID,则使用默认的租户ID,或者直接报异常信息 return StringUtils.isNotBlank(tenant)?tenant:DEFAULT_TENANT; } @Override public boolean validateExistingCurrentSessions() { return true; }}
此类的逻辑非常简单,就是从ThreadLocal中获取当前设置的租户标识符有了租户标识符解析类之后,我们需要扩展租户数据源提供类,实现从数据库动态查询租户数据源信息,其源码如下:
@Slf4j@Configurationpublic class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl{ private static final long serialVersionUID = -7522287771874314380L; @Autowired private MasterTenantRepository masterTenantRepository; private Map<String,DataSource> dataSources = new TreeMap<>(); @Override protected DataSource selectAnyDataSource() { if(dataSources.isEmpty()){ List<MasterTenant> tenants = masterTenantRepository.findAll(); tenants.forEach(masterTenant->{ dataSources.put(masterTenant.getTenant(), DataSourceUtils.wrapperDataSource(masterTenant)); }); } return dataSources.values().iterator().next(); } @Override protected DataSource selectDataSource(String tenant) { if(!dataSources.containsKey(tenant)){ List<MasterTenant> tenants = masterTenantRepository.findAll(); tenants.forEach(masterTenant->{ dataSources.put(masterTenant.getTenant(),DataSourceUtils.wrapperDataSource(masterTenant)); }); } return dataSources.get(tenant); }}
在该类中,通过查询租户数据源库,动态获得租户数据源信息,为租户业务模块的数据源配置提供数据数据支持。最后,我们还需要提供租户业务模块数据源配置,这是整个项目核心的地方,其代码如下:
@Slf4j@Configuration@EnableTransactionManagement@ComponentScan(basePackages = { "com.ramostear.una.saas.tenant.model", "com.ramostear.una.saas.tenant.repository"})@EnableJpaRepositories(basePackages = { "com.ramostear.una.saas.tenant.repository", "com.ramostear.una.saas.tenant.service"},entityManagerFactoryRef = "tenantEntityManagerFactory",transactionManagerRef = "tenantTransactionManager")public class TenantDataSourceConfig { @Bean("jpaVendorAdapter") public JpaVendorAdapter jpaVendorAdapter(){ return new HibernateJpaVendorAdapter(); } @Bean(name = "tenantTransactionManager") public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(entityManagerFactory); return transactionManager; } @Bean(name = "datasourceBasedMultiTenantConnectionProvider") @ConditionalOnBean(name = "masterEntityManagerFactory") public MultiTenantConnectionProvider multiTenantConnectionProvider(){ return new DataSourceBasedMultiTenantConnectionProviderImpl(); } @Bean(name = "currentTenantIdentifierResolver") public CurrentTenantIdentifierResolver currentTenantIdentifierResolver(){ return new CurrentTenantIdentifierResolverImpl(); } @Bean(name = "tenantEntityManagerFactory") @ConditionalOnBean(name = "datasourceBasedMultiTenantConnectionProvider") public LocalContainerEntityManagerFactoryBean entityManagerFactory( @Qualifier("datasourceBasedMultiTenantConnectionProvider")MultiTenantConnectionProvider connectionProvider, @Qualifier("currentTenantIdentifierResolver")CurrentTenantIdentifierResolver tenantIdentifierResolver ){ LocalContainerEntityManagerFactoryBean localBean = new LocalContainerEntityManagerFactoryBean(); localBean.setPackagesToScan( new String[]{ User.class.getPackage().getName(), UserRepository.class.getPackage().getName(), UserService.class.getPackage().getName() } ); localBean.setJpaVendorAdapter(jpaVendorAdapter()); localBean.setPersistenceUnitName("tenant-database-persistence-unit"); Map<String,Object> properties = new HashMap<>(); properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA); properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER,connectionProvider); properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER,tenantIdentifierResolver); properties.put(Environment.DIALECT,"org.hibernate.dialect.MySQL5Dialect"); properties.put(Environment.SHOW_SQL,true); properties.put(Environment.FORMAT_SQL,true); properties.put(Environment.HBM2DDL_AUTO,"update"); localBean.setJpaPropertyMap(properties); return localBean; }}
在改配置文件中,大部分内容与主数据源的配置相同,唯一的区别是租户标识解析器与租户数据源补给源的设置,它将告诉Hibernate在执行数据库操作命令前,应该设置什么样的数据库连接信息,以及用户名和密码等信息。
/** * @author : Created by Tan Chaohong (alias:ramostear) * @create-time 2019/5/27 0027-0:18 * @modify by : * @since: */@Controllerpublic class LoginController { @Autowired private UserService userService; @GetMapping("/login.html") public String login(){ return "/login"; } @PostMapping("/login") public String login(@RequestParam(name = "username") String username, @RequestParam(name = "password")String password, ModelMap model){ System.out.println("tenant:"+TenantContextHolder.getTenant()); User user = userService.findByUsername(username); if(user != null){ if(user.getPassword().equals(password)){ model.put("user",user); return "/index"; }else{ return "/login"; } }else{ return "/login"; } }}
在启动项目之前,我们需要为主数据源创建对应的数据库和数据表,用于存放租户数据源信息,同时还需要提供一个租户业务模块数据库和数据表,用来存放租户业务数据。一切准备就绪后,启动项目,在浏览器中输入:http://localhost:8080/login.html关键词:平台,核心,技术,指南,租户