├── src └── main │ ├── webapp │ ├── index.jsp │ └── WEB-INF │ │ └── web.xml │ ├── java │ └── com │ │ └── closer │ │ ├── tenant │ │ ├── service │ │ │ ├── TenantSupport.java │ │ │ └── TenantService.java │ │ ├── repository │ │ │ ├── TenantRepository.java │ │ │ └── impl │ │ │ │ └── TenantRepositoryImpl.java │ │ ├── event │ │ │ └── TenantCreateEvent.java │ │ ├── domain │ │ │ └── Tenant.java │ │ └── web │ │ │ └── TenantController.java │ │ ├── department │ │ ├── repository │ │ │ └── DepartmentRepository.java │ │ ├── service │ │ │ └── DepartmentService.java │ │ ├── web │ │ │ └── DepartmentController.java │ │ └── domain │ │ │ └── Department.java │ │ ├── employee │ │ ├── repository │ │ │ └── EmployeeRepository.java │ │ ├── web │ │ │ └── EmployeeController.java │ │ ├── service │ │ │ └── EmployeeService.java │ │ ├── domain │ │ │ └── Employee.java │ │ └── job │ │ │ └── HappyBirthdayJob.java │ │ └── common │ │ ├── handler │ │ ├── DynamicRoutingDataSource.java │ │ ├── TableMapperInterceptor.java │ │ ├── TableProvider.java │ │ ├── TenantHandlerInterceptor.java │ │ ├── TableHandler.java │ │ └── DistributedIdentifierGenerator.java │ │ ├── view │ │ └── View.java │ │ ├── repository │ │ ├── BaseRepository.java │ │ └── BaseTenantRepository.java │ │ ├── constant │ │ └── IDG.java │ │ ├── domain │ │ ├── BaseTenantDomain.java │ │ └── BaseDomain.java │ │ ├── config │ │ ├── ServiceConfig.java │ │ ├── WebInitializer.java │ │ ├── QuartzConfig.java │ │ ├── WebConfig.java │ │ ├── RDMSConfig.java │ │ └── RedisConfig.java │ │ ├── helper │ │ └── ContextHelper.java │ │ └── service │ │ ├── BaseTenantService.java │ │ └── BaseService.java │ ├── resources │ ├── idg.lua │ ├── log4j.properties │ └── quartz.properties │ └── init-sql │ ├── hsqldb │ └── quartz-init.sql │ └── mysql │ ├── tables_mysql.sql │ └── tables_mysql_innodb.sql ├── .gitignore ├── dynamic-table-mapper-postman.json ├── pom.xml └── README.md /src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Hello World!

4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/com/closer/tenant/service/TenantSupport.java: -------------------------------------------------------------------------------- 1 | package com.closer.tenant.service; 2 | 3 | /** 4 | * Created by closer on 2016/1/27. 5 | */ 6 | public interface TenantSupport { 7 | 8 | Class[] getEntities(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/idg.lua: -------------------------------------------------------------------------------- 1 | local sequence = redis.call("INCR", ARGV[1]) 2 | local expire = redis.call("TTL", ARGV[1]) 3 | if expire == -1 then 4 | redis.call("EXPIRE", ARGV[1], ARGV[2]) 5 | expire = ARGV[2] 6 | end 7 | 8 | return expire .. "," .. sequence -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | Archetype Created Web Application 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # maven ignore 2 | target/ 3 | *.jar 4 | *.war 5 | *.zip 6 | *.tar 7 | *.tar.gz 8 | 9 | # eclipse ignore 10 | .settings/ 11 | .project 12 | .classpath 13 | 14 | # idea ignore 15 | .idea/ 16 | *.ipr 17 | *.iml 18 | *.iws 19 | 20 | # temp ignore 21 | *.log 22 | *.cache 23 | *.diff 24 | *.patch 25 | *.tmp 26 | 27 | # system ignore 28 | .DS_Store 29 | Thumbs.db 30 | -------------------------------------------------------------------------------- /src/main/java/com/closer/tenant/repository/TenantRepository.java: -------------------------------------------------------------------------------- 1 | package com.closer.tenant.repository; 2 | 3 | import com.closer.common.repository.BaseRepository; 4 | import com.closer.tenant.domain.Tenant; 5 | import org.springframework.stereotype.Repository; 6 | 7 | /** 8 | * Created by closer on 2016/1/27. 9 | */ 10 | @Repository 11 | public interface TenantRepository extends BaseRepository { 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/closer/department/repository/DepartmentRepository.java: -------------------------------------------------------------------------------- 1 | package com.closer.department.repository; 2 | 3 | import com.closer.common.repository.BaseTenantRepository; 4 | import com.closer.department.domain.Department; 5 | import org.springframework.stereotype.Repository; 6 | 7 | /** 8 | * Created by closer on 2016/1/20. 9 | */ 10 | @Repository 11 | public interface DepartmentRepository extends BaseTenantRepository { 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/closer/employee/repository/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | package com.closer.employee.repository; 2 | 3 | import com.closer.common.repository.BaseTenantRepository; 4 | import com.closer.employee.domain.Employee; 5 | import org.springframework.stereotype.Repository; 6 | 7 | /** 8 | * 员工Dao 9 | * Created by closer on 2016/1/4. 10 | */ 11 | @Repository 12 | public interface EmployeeRepository extends BaseTenantRepository { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/handler/DynamicRoutingDataSource.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.handler; 2 | 3 | import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; 4 | 5 | /** 6 | * Created by closer on 2016/1/27. 7 | */ 8 | public class DynamicRoutingDataSource extends AbstractRoutingDataSource{ 9 | 10 | @Override 11 | protected Object determineCurrentLookupKey() { 12 | return TableProvider.getDataSoureName(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/closer/tenant/event/TenantCreateEvent.java: -------------------------------------------------------------------------------- 1 | package com.closer.tenant.event; 2 | 3 | import com.closer.tenant.domain.Tenant; 4 | 5 | /** 6 | * 公司新增事件 7 | * Created by closer on 2016/1/5. 8 | */ 9 | public class TenantCreateEvent { 10 | private Tenant tenant; 11 | 12 | public TenantCreateEvent(Tenant tenant) { 13 | this.tenant = tenant; 14 | } 15 | 16 | public Tenant getTenant() { 17 | return tenant; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/view/View.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.view; 2 | 3 | /** 4 | * 定义的公共View供@JsonView使用 5 | * Created by closer on 2016/1/24. 6 | */ 7 | public class View { 8 | 9 | public interface Detail { 10 | } 11 | 12 | public interface List { 13 | } 14 | 15 | public interface Eager { 16 | } 17 | 18 | public interface EagerDetail extends Eager { 19 | } 20 | 21 | public interface EagerList extends Eager { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/handler/TableMapperInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.handler; 2 | 3 | import org.hibernate.EmptyInterceptor; 4 | 5 | /** 6 | * 表映射拦截器 7 | * Created by Zhang Jinlong(150429) on 2016/1/4. 8 | */ 9 | public class TableMapperInterceptor extends EmptyInterceptor { 10 | 11 | @Override 12 | public String onPrepareStatement(String sql) { 13 | return sql.replace(TableProvider.PREFIX, TableProvider.getTablePrefix()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/repository/BaseRepository.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.repository; 2 | 3 | import com.closer.common.domain.BaseDomain; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.repository.NoRepositoryBean; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 基础Repository 11 | * Created by closer on 2016/1/5. 12 | */ 13 | @NoRepositoryBean 14 | public interface BaseRepository,I extends Serializable> extends JpaRepository{ 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/constant/IDG.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.constant; 2 | 3 | /** 4 | * 主健生成策略 5 | * Created by Zhang Jinlong(150429) on 2016/2/14. 6 | */ 7 | public final class IDG { 8 | 9 | public static final String UUID = "uuid2"; 10 | public static final String IDENTITY = "identity"; 11 | public static final String ASSIGNED = "assigned"; 12 | public static final String DISTRIBUTED_IDENTITY = "com.closer.common.handler.DistributedIdentifierGenerator"; 13 | 14 | private IDG() { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/repository/BaseTenantRepository.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.repository; 2 | 3 | import com.closer.common.domain.BaseTenantDomain; 4 | import org.springframework.data.repository.NoRepositoryBean; 5 | 6 | import java.io.Serializable; 7 | import java.util.List; 8 | 9 | /** 10 | * 租户业务基础Dao 11 | * Created by closer on 2016/1/27. 12 | */ 13 | @NoRepositoryBean 14 | public interface BaseTenantRepository,I extends Serializable> extends BaseRepository { 15 | 16 | T findByIdAndTenant(I id, long tenant); 17 | 18 | List findByTenant(long tenant); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/closer/department/service/DepartmentService.java: -------------------------------------------------------------------------------- 1 | package com.closer.department.service; 2 | 3 | import com.closer.common.service.BaseTenantService; 4 | import com.closer.department.domain.Department; 5 | import com.closer.tenant.service.TenantSupport; 6 | import org.springframework.stereotype.Service; 7 | 8 | /** 9 | * Created by closer on 2016/1/20. 10 | */ 11 | @Service 12 | public class DepartmentService extends BaseTenantService implements TenantSupport { 13 | 14 | 15 | @Override 16 | public Class[] getEntities() { 17 | return new Class[]{Department.class}; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/domain/BaseTenantDomain.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.domain; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.MappedSuperclass; 5 | import java.io.Serializable; 6 | 7 | /** 8 | * Created by closer on 2016/1/27. 9 | */ 10 | @MappedSuperclass 11 | public abstract class BaseTenantDomain extends BaseDomain { 12 | 13 | @Column(updatable = false) 14 | private Long tenant; 15 | 16 | public Long getTenant() { 17 | return tenant; 18 | } 19 | 20 | public void setTenant(Long tenant) { 21 | this.tenant = tenant; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=INFO, stdout, logfile 2 | 3 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 4 | log4j.appender.stdout.Target=System.out 5 | log4j.appender.stdout.Threshold=INFO 6 | log4j.appender.stdout.ImmediateFlush=true 7 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 8 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %X{RequestId} - %m%n 9 | 10 | log4j.appender.logfile=org.apache.log4j.RollingFileAppender 11 | log4j.appender.logfile.File=logs/web.log 12 | #100*1024*1024 13 | log4j.appender.logfile.MaxFileSize=104857600 14 | log4j.appender.logfile.MaxBackupIndex=5 15 | log4j.appender.logfile.layout=org.apache.log4j.PatternLayout 16 | log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n -------------------------------------------------------------------------------- /src/main/java/com/closer/common/config/ServiceConfig.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.config; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.scheduling.annotation.EnableAsync; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.stereotype.Repository; 8 | import org.springframework.web.bind.annotation.ControllerAdvice; 9 | 10 | /** 11 | * Created by closer on 2016/1/3. 12 | */ 13 | @Configuration 14 | @EnableAsync(proxyTargetClass = true) 15 | @ComponentScan(basePackages = "com.closer", 16 | excludeFilters = @ComponentScan.Filter({Controller.class, Configuration.class, 17 | Repository.class, ControllerAdvice.class})) 18 | public class ServiceConfig { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/config/WebInitializer.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.config; 2 | 3 | import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; 4 | 5 | /** 6 | * Created by closer on 2016/1/3. 7 | */ 8 | public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { 9 | @Override 10 | protected Class[] getRootConfigClasses() { 11 | return null; 12 | } 13 | 14 | @Override 15 | protected Class[] getServletConfigClasses() { 16 | return new Class[]{WebConfig.class, ServiceConfig.class, RDMSConfig.class, 17 | RedisConfig.class, QuartzConfig.class}; 18 | } 19 | 20 | @Override 21 | protected String[] getServletMappings() { 22 | return new String[]{"/"}; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/helper/ContextHelper.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.helper; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * Created by closer on 2016/2/4. 10 | */ 11 | @Component 12 | public class ContextHelper implements ApplicationContextAware { 13 | 14 | private static ApplicationContext _applicationContext; 15 | 16 | @Override 17 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 18 | _applicationContext = applicationContext; 19 | } 20 | 21 | public static ApplicationContext applicationContext() { 22 | return _applicationContext; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/closer/tenant/service/TenantService.java: -------------------------------------------------------------------------------- 1 | package com.closer.tenant.service; 2 | 3 | import com.closer.common.service.BaseService; 4 | import com.closer.tenant.domain.Tenant; 5 | import com.closer.tenant.event.TenantCreateEvent; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.context.ApplicationEventPublisher; 8 | import org.springframework.stereotype.Service; 9 | 10 | /** 11 | * Created by closer on 2016/1/27. 12 | */ 13 | @Service 14 | public class TenantService extends BaseService { 15 | 16 | @Autowired 17 | private ApplicationEventPublisher publisher; 18 | 19 | @Override 20 | public Tenant add(Tenant tenant) { 21 | super.add(tenant); 22 | publisher.publishEvent(new TenantCreateEvent(tenant)); 23 | return tenant; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/quartz.properties: -------------------------------------------------------------------------------- 1 | #调度集群名称,集群需要保持一致 2 | org.quartz.scheduler.instanceName = QuartzScheduler 3 | #实例id,这里使用AUTO表示自动生成 4 | org.quartz.scheduler.instanceId = AUTO 5 | 6 | org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool 7 | org.quartz.threadPool.threadCount = 10 8 | org.quartz.threadPool.threadPriority = 5 9 | org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true 10 | 11 | org.quartz.jobStore.misfireThreshold = 60000 12 | org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX 13 | org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate 14 | org.quartz.jobStore.tablePrefix = QRTZ_ 15 | org.quartz.jobStore.maxMisfiresToHandleAtATime=10 16 | #是否为集群模式 17 | org.quartz.jobStore.isClustered = true 18 | #心跳检测的频率,频率越小将越早发现集群其它机器失效 19 | org.quartz.jobStore.clusterCheckinInterval = 20000 20 | 21 | #新版本检查 22 | org.quartz.scheduler.skipUpdateCheck:true -------------------------------------------------------------------------------- /src/main/java/com/closer/tenant/domain/Tenant.java: -------------------------------------------------------------------------------- 1 | package com.closer.tenant.domain; 2 | 3 | import com.closer.common.constant.IDG; 4 | import com.closer.common.domain.BaseDomain; 5 | import org.hibernate.annotations.GenericGenerator; 6 | 7 | import javax.persistence.Entity; 8 | import javax.persistence.Table; 9 | 10 | /** 11 | * Created by closer on 2016/1/27. 12 | */ 13 | @Entity 14 | @Table(name = "t_tenant") 15 | @GenericGenerator(name = "id", strategy = IDG.IDENTITY) 16 | public class Tenant extends BaseDomain { 17 | 18 | private String name; 19 | 20 | private String dataSourceName; 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | public void setName(String name) { 27 | this.name = name; 28 | } 29 | 30 | public String getDataSourceName() { 31 | return dataSourceName; 32 | } 33 | 34 | public void setDataSourceName(String dataSourceName) { 35 | this.dataSourceName = dataSourceName; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/closer/tenant/web/TenantController.java: -------------------------------------------------------------------------------- 1 | package com.closer.tenant.web; 2 | 3 | import com.closer.tenant.domain.Tenant; 4 | import com.closer.tenant.service.TenantService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import java.util.Map; 9 | 10 | /** 11 | * Created by closer on 2016/1/27. 12 | */ 13 | @RestController 14 | @RequestMapping("/tenants") 15 | public class TenantController { 16 | 17 | @Autowired 18 | private TenantService tenantService; 19 | 20 | @RequestMapping(method = RequestMethod.POST) 21 | public Tenant add(@RequestBody Tenant tenant) { 22 | return tenantService.add(tenant); 23 | } 24 | 25 | @RequestMapping(value = "/{id}", method = RequestMethod.GET) 26 | public Tenant get(@PathVariable long id) { 27 | return tenantService.findStrictOne(id); 28 | } 29 | 30 | @RequestMapping(value = "/{id}", method = RequestMethod.PUT) 31 | public Tenant add(@PathVariable long id, @RequestBody Map tenantMap) { 32 | return tenantService.update(id, tenantMap); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/service/BaseTenantService.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.service; 2 | 3 | import com.closer.common.domain.BaseTenantDomain; 4 | import com.closer.common.handler.TableProvider; 5 | import com.closer.common.repository.BaseTenantRepository; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | /** 12 | * Created by closer on 2016/1/27. 13 | */ 14 | public abstract class BaseTenantService, I extends Serializable> extends BaseService { 15 | 16 | @Autowired 17 | private BaseTenantRepository baseTenantRepository; 18 | 19 | @Override 20 | public T findOne(I id) { 21 | return baseTenantRepository.findByIdAndTenant(id, TableProvider.getTenantId()); 22 | } 23 | 24 | @Override 25 | public List findAll() { 26 | return baseTenantRepository.findByTenant(TableProvider.getTenantId()); 27 | } 28 | 29 | @Override 30 | public T add(T t) { 31 | t.setTenant(TableProvider.getTenantId()); 32 | return super.add(t); 33 | } 34 | 35 | @Override 36 | public boolean exists(I id) { 37 | return findOne(id) != null; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/closer/employee/web/EmployeeController.java: -------------------------------------------------------------------------------- 1 | package com.closer.employee.web; 2 | 3 | import com.closer.common.view.View; 4 | import com.closer.employee.domain.Employee; 5 | import com.closer.employee.service.EmployeeService; 6 | import com.fasterxml.jackson.annotation.JsonView; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * 员工控制器 14 | * Created by closer on 2016/1/4. 15 | */ 16 | @RestController 17 | @RequestMapping("/employees") 18 | public class EmployeeController { 19 | 20 | @Autowired 21 | private EmployeeService service; 22 | 23 | @JsonView(View.EagerDetail.class) 24 | @RequestMapping(value = "/{id}",method = RequestMethod.GET) 25 | public Employee get(@PathVariable("id") long id) { 26 | return service.findOne(id); 27 | } 28 | 29 | @JsonView(View.List.class) 30 | @RequestMapping(method = RequestMethod.GET) 31 | public List list() { 32 | return service.findAll(); 33 | } 34 | 35 | @RequestMapping(method = RequestMethod.POST) 36 | public Employee add(@RequestBody Employee employee) { 37 | return service.add(employee); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/domain/BaseDomain.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.domain; 2 | 3 | import org.springframework.data.annotation.CreatedDate; 4 | import org.springframework.data.annotation.LastModifiedDate; 5 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 6 | 7 | import javax.persistence.*; 8 | 9 | /** 10 | * Created by closer on 2016/1/5. 11 | */ 12 | @MappedSuperclass 13 | @EntityListeners(AuditingEntityListener.class) 14 | public abstract class BaseDomain { 15 | 16 | @Id 17 | @GeneratedValue(generator = "id") 18 | @Column(length = 36) 19 | private I id; 20 | 21 | @LastModifiedDate 22 | private long updateTime; 23 | 24 | @CreatedDate 25 | @Column(updatable = false) 26 | private long createTime; 27 | 28 | public I getId() { 29 | return id; 30 | } 31 | 32 | public void setId(I id) { 33 | this.id = id; 34 | } 35 | 36 | public long getUpdateTime() { 37 | return updateTime; 38 | } 39 | 40 | public void setUpdateTime(long updateTime) { 41 | this.updateTime = updateTime; 42 | } 43 | 44 | public long getCreateTime() { 45 | return createTime; 46 | } 47 | 48 | public void setCreateTime(long createTime) { 49 | this.createTime = createTime; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/closer/tenant/repository/impl/TenantRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.closer.tenant.repository.impl; 2 | 3 | import com.closer.tenant.domain.Tenant; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.cache.annotation.CacheConfig; 6 | import org.springframework.cache.annotation.CacheEvict; 7 | import org.springframework.cache.annotation.Cacheable; 8 | import org.springframework.data.jpa.repository.support.SimpleJpaRepository; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import javax.persistence.EntityManager; 12 | 13 | /** 14 | * Created by closer on 2016/1/27. 15 | */ 16 | @Repository 17 | @CacheConfig(cacheNames = "tenants") 18 | public class TenantRepositoryImpl extends SimpleJpaRepository { 19 | 20 | @Autowired 21 | public TenantRepositoryImpl(EntityManager entityManager) { 22 | super(Tenant.class, entityManager); 23 | } 24 | 25 | @Cacheable 26 | @Override 27 | public Tenant findOne(Long id) { 28 | return super.findOne(id); 29 | } 30 | 31 | @CacheEvict 32 | @Override 33 | public void delete(Long id) { 34 | super.delete(id); 35 | } 36 | 37 | @CacheEvict(key = "#p0.id") 38 | @Override 39 | public Tenant save(Tenant entity) { 40 | return super.save(entity); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/closer/department/web/DepartmentController.java: -------------------------------------------------------------------------------- 1 | package com.closer.department.web; 2 | 3 | import com.closer.department.domain.Department; 4 | import com.closer.department.service.DepartmentService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | /** 12 | * 部门Controller 13 | * Created by closer on 2016/1/20. 14 | */ 15 | @RestController 16 | @RequestMapping("/departments") 17 | public class DepartmentController { 18 | 19 | @Autowired 20 | private DepartmentService service; 21 | 22 | @RequestMapping(value = "/{id}", method = RequestMethod.GET) 23 | public Department get(@PathVariable long id) { 24 | return service.findStrictOne(id); 25 | } 26 | 27 | @RequestMapping(method = RequestMethod.GET) 28 | public List list() { 29 | return service.findAll(); 30 | } 31 | 32 | @RequestMapping(method = RequestMethod.POST) 33 | public Department add(@RequestBody Department department) { 34 | return service.add(department); 35 | } 36 | 37 | @RequestMapping(value = "/{id}", method = RequestMethod.PUT) 38 | public Department update(@PathVariable long id, 39 | @RequestBody Map departmentMap) { 40 | return service.update(id, departmentMap); 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/handler/TableProvider.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.handler; 2 | 3 | import com.closer.tenant.domain.Tenant; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | /** 7 | * Created by Zhang Jinlong(150429) on 2016/1/4. 8 | */ 9 | public class TableProvider { 10 | 11 | private static final String DEFAULT_PREFIX = "default"; 12 | public static final String PREFIX = "#org#"; 13 | public static final String PREFIX_ = PREFIX + "_"; 14 | private static ThreadLocal tenantThreadLocal = new ThreadLocal<>(); 15 | 16 | public static void setTenant(Tenant tenant) { 17 | tenantThreadLocal.set(tenant); 18 | } 19 | 20 | public static String getTablePrefix() { 21 | Tenant tenant = tenantThreadLocal.get(); 22 | if (tenant == null) { 23 | return ""; 24 | } 25 | return StringUtils.defaultString("T"+String.valueOf(getTableNum()), DEFAULT_PREFIX); 26 | } 27 | 28 | public static long getTableNum() { 29 | return getTenantId() & 7; 30 | } 31 | 32 | public static long getTenantId() { 33 | Tenant tenant = tenantThreadLocal.get(); 34 | if (tenant == null) { 35 | throw new RuntimeException("未标识您请求的是租户id"); 36 | } 37 | return tenant.getId(); 38 | } 39 | 40 | public static String getDataSoureName() { 41 | Tenant tenant = tenantThreadLocal.get(); 42 | if (tenant == null) { 43 | return ""; 44 | } 45 | return tenant.getDataSourceName(); 46 | } 47 | 48 | public static void clear() { 49 | tenantThreadLocal.remove(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/closer/department/domain/Department.java: -------------------------------------------------------------------------------- 1 | package com.closer.department.domain; 2 | 3 | import com.closer.common.constant.IDG; 4 | import com.closer.common.domain.BaseTenantDomain; 5 | import com.closer.common.handler.TableProvider; 6 | import com.closer.employee.domain.Employee; 7 | import com.fasterxml.jackson.annotation.JsonIgnore; 8 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 9 | import org.hibernate.annotations.GenericGenerator; 10 | 11 | import javax.persistence.*; 12 | import java.util.List; 13 | 14 | /** 15 | * 部门实体 16 | * Created by closer on 2016/1/20. 17 | */ 18 | @Entity 19 | @Table(name = TableProvider.PREFIX_ + "department") 20 | @GenericGenerator(name = "id", strategy = IDG.DISTRIBUTED_IDENTITY) 21 | public class Department extends BaseTenantDomain { 22 | 23 | private String name; 24 | 25 | @JsonIgnore 26 | @OneToMany(mappedBy = "department", cascade = CascadeType.DETACH) 27 | private List employees; 28 | 29 | @OneToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY) 30 | @JsonIgnoreProperties("department") 31 | private Employee manager; 32 | 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | public void setName(String name) { 38 | this.name = name; 39 | } 40 | 41 | public List getEmployees() { 42 | return employees; 43 | } 44 | 45 | public void setEmployees(List employees) { 46 | this.employees = employees; 47 | } 48 | 49 | public Employee getManager() { 50 | return manager; 51 | } 52 | 53 | public void setManager(Employee manager) { 54 | this.manager = manager; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/closer/employee/service/EmployeeService.java: -------------------------------------------------------------------------------- 1 | package com.closer.employee.service; 2 | 3 | import com.closer.common.service.BaseTenantService; 4 | import com.closer.employee.domain.Employee; 5 | import com.closer.employee.job.HappyBirthdayJob; 6 | import com.closer.tenant.service.TenantSupport; 7 | import org.quartz.Scheduler; 8 | import org.quartz.SchedulerException; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.ApplicationEventPublisher; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.transaction.event.TransactionalEventListener; 13 | 14 | import java.util.Map; 15 | 16 | /** 17 | * 员工Service 18 | * Created by Zhang Jinlong(150429) on 2016/1/4. 19 | */ 20 | @Service 21 | public class EmployeeService extends BaseTenantService implements TenantSupport { 22 | 23 | @Autowired 24 | private ApplicationEventPublisher eventPublisher; 25 | 26 | @Autowired 27 | private Scheduler scheduler; 28 | 29 | @Override 30 | public Class[] getEntities() { 31 | return new Class[]{Employee.class}; 32 | } 33 | 34 | @Override 35 | public Employee update(Long id, Map map) { 36 | Employee employee = super.update(id, map); 37 | eventPublisher.publishEvent(employee); 38 | return employee; 39 | } 40 | 41 | @Override 42 | public Employee add(Employee employee) { 43 | employee = super.add(employee); 44 | eventPublisher.publishEvent(employee); 45 | return employee; 46 | } 47 | 48 | @TransactionalEventListener 49 | public void handleEmployee(Employee employee) throws SchedulerException { 50 | HappyBirthdayJob.trigger(scheduler, employee); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/config/QuartzConfig.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.config; 2 | 3 | import com.closer.employee.job.HappyBirthdayJob; 4 | import org.quartz.JobDetail; 5 | import org.quartz.Scheduler; 6 | import org.quartz.SchedulerException; 7 | import org.springframework.context.ApplicationContext; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.core.io.ClassPathResource; 11 | import org.springframework.scheduling.quartz.SchedulerFactoryBean; 12 | 13 | import javax.sql.DataSource; 14 | 15 | import static org.quartz.JobBuilder.newJob; 16 | 17 | /** 18 | * Created by closer on 2016/2/22. 19 | */ 20 | @Configuration 21 | public class QuartzConfig { 22 | 23 | public static final String EMPLOYEE_HAPPY_BIRTHDAY = "Employee-HappyBirthdayJob"; 24 | public static final String JOB = "Job"; 25 | 26 | @Bean 27 | public SchedulerFactoryBean quartzScheduler(DataSource dataSource, ApplicationContext applicationContext) throws Exception { 28 | SchedulerFactoryBean scheduler = new SchedulerFactoryBean(); 29 | scheduler.setDataSource(dataSource); 30 | scheduler.setApplicationContext(applicationContext); 31 | scheduler.setConfigLocation(new ClassPathResource("quartz.properties")); 32 | scheduler.afterPropertiesSet(); 33 | return scheduler; 34 | } 35 | 36 | @Bean 37 | public JobDetail happyBirthday(Scheduler scheduler) throws SchedulerException { 38 | JobDetail jobDetail = newJob(HappyBirthdayJob.class) 39 | .withIdentity(JOB, EMPLOYEE_HAPPY_BIRTHDAY) 40 | .requestRecovery() 41 | .storeDurably() 42 | .build(); 43 | scheduler.addJob(jobDetail, true); 44 | return jobDetail; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/handler/TenantHandlerInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.handler; 2 | 3 | import com.closer.tenant.domain.Tenant; 4 | import com.closer.tenant.service.TenantService; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.apache.commons.lang3.math.NumberUtils; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.web.servlet.HandlerInterceptor; 9 | import org.springframework.web.servlet.ModelAndView; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | 14 | /** 15 | * Created by closer on 2016/1/27. 16 | */ 17 | public class TenantHandlerInterceptor implements HandlerInterceptor { 18 | 19 | @Autowired 20 | private TenantService tenantService; 21 | 22 | @Override 23 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 24 | if (request.getRequestURI().startsWith("/tenants")) { 25 | return true; 26 | } 27 | String tenantIdStr = request.getHeader("tenant"); 28 | if (StringUtils.isBlank(tenantIdStr) || !NumberUtils.isNumber(tenantIdStr)) { 29 | throw new RuntimeException("请求头部需要包含有租户的id,类型为整型"); 30 | } 31 | long tenantId = NumberUtils.toLong(tenantIdStr); 32 | Tenant tenant = tenantService.findOne(tenantId); 33 | if (tenant == null) { 34 | throw new RuntimeException("指定的租户不存在"); 35 | } 36 | TableProvider.setTenant(tenant); 37 | return true; 38 | } 39 | 40 | @Override 41 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 42 | 43 | } 44 | 45 | @Override 46 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 47 | TableProvider.clear(); 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/com/closer/employee/domain/Employee.java: -------------------------------------------------------------------------------- 1 | package com.closer.employee.domain; 2 | 3 | 4 | import com.closer.common.constant.IDG; 5 | import com.closer.common.domain.BaseTenantDomain; 6 | import com.closer.common.handler.TableProvider; 7 | import com.closer.common.view.View; 8 | import com.closer.department.domain.Department; 9 | import com.fasterxml.jackson.annotation.JsonView; 10 | import org.apache.commons.lang3.time.DateUtils; 11 | import org.hibernate.annotations.GenericGenerator; 12 | 13 | import javax.persistence.*; 14 | import java.util.Calendar; 15 | import java.util.Date; 16 | 17 | /** 18 | * 员工实体 19 | * Created by Zhang Jinlong(150429) on 2016/1/4. 20 | */ 21 | @Entity 22 | @Table(name = TableProvider.PREFIX_ + "employee") 23 | @GenericGenerator(name = "id", strategy = IDG.DISTRIBUTED_IDENTITY) 24 | public class Employee extends BaseTenantDomain { 25 | 26 | private String name; 27 | 28 | private String enName; 29 | 30 | private Long birthday; 31 | 32 | @JsonView(View.EagerDetail.class) 33 | @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.DETACH) 34 | private Department department; 35 | 36 | public String getName() { 37 | return name; 38 | } 39 | 40 | public void setName(String name) { 41 | this.name = name; 42 | } 43 | 44 | public String getEnName() { 45 | return enName; 46 | } 47 | 48 | public void setEnName(String enName) { 49 | this.enName = enName; 50 | } 51 | 52 | public Department getDepartment() { 53 | return department; 54 | } 55 | 56 | public void setDepartment(Department department) { 57 | this.department = department; 58 | } 59 | 60 | public Date getBirthday() { 61 | if (birthday == null) { 62 | return null; 63 | } 64 | return new Date(birthday); 65 | } 66 | 67 | public void setBirthday(Date birthday) { 68 | if (birthday == null) { 69 | this.birthday = null; 70 | } 71 | this.birthday = DateUtils.truncate(birthday, Calendar.HOUR).getTime(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/closer/employee/job/HappyBirthdayJob.java: -------------------------------------------------------------------------------- 1 | package com.closer.employee.job; 2 | 3 | import com.closer.common.config.QuartzConfig; 4 | import com.closer.employee.domain.Employee; 5 | import org.quartz.*; 6 | 7 | import java.util.Calendar; 8 | import java.util.Date; 9 | 10 | import static org.quartz.CronScheduleBuilder.cronSchedule; 11 | import static org.quartz.TriggerBuilder.newTrigger; 12 | 13 | /** 14 | * 发送生日福语任务 15 | * Created by closer on 2016/2/23. 16 | */ 17 | public class HappyBirthdayJob implements Job { 18 | 19 | public static final String NAME = "Name"; 20 | 21 | @Override 22 | public void execute(JobExecutionContext context) throws JobExecutionException { 23 | JobDataMap data = context.getTrigger().getJobDataMap(); 24 | String name = (String) data.get(NAME); 25 | System.out.println("--------------------------------"); 26 | System.out.println("Happy Birthday to " + name); 27 | System.out.println("--------------------------------"); 28 | } 29 | 30 | public static void trigger(Scheduler scheduler, Employee employee) throws SchedulerException { 31 | long id = employee.getId(); 32 | String triggerName = String.valueOf(id); 33 | scheduler.unscheduleJob( 34 | TriggerKey.triggerKey(triggerName, 35 | QuartzConfig.EMPLOYEE_HAPPY_BIRTHDAY) 36 | ); 37 | 38 | Date birthday = employee.getBirthday(); 39 | if (birthday != null) { 40 | Calendar calendar = Calendar.getInstance(); 41 | calendar.setTime(birthday); 42 | String cronExpression = String.format("0 10 23 %d %d ?", 43 | calendar.get(Calendar.DATE), calendar.get(Calendar.MONTH)) + 1; 44 | Trigger trigger = newTrigger() 45 | .forJob(QuartzConfig.JOB, QuartzConfig.EMPLOYEE_HAPPY_BIRTHDAY) 46 | .withIdentity(triggerName, QuartzConfig.EMPLOYEE_HAPPY_BIRTHDAY) 47 | .usingJobData(NAME, employee.getName()) 48 | .startNow() 49 | .withSchedule(cronSchedule(cronExpression)) 50 | .build(); 51 | scheduler.scheduleJob(trigger); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.config; 2 | 3 | import com.closer.common.handler.TenantHandlerInterceptor; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 6 | import com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module; 7 | import org.hibernate.MappingException; 8 | import org.hibernate.engine.spi.Mapping; 9 | import org.hibernate.id.factory.IdentifierGeneratorFactory; 10 | import org.hibernate.type.Type; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.ComponentScan; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.http.converter.HttpMessageConverter; 15 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 16 | import org.springframework.stereotype.Controller; 17 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 18 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 19 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 20 | 21 | import java.util.List; 22 | 23 | /** 24 | * Created by closer on 2016/1/3. 25 | */ 26 | @Configuration 27 | @EnableWebMvc 28 | @ComponentScan(value = "com.closer", includeFilters = @ComponentScan.Filter(Controller.class)) 29 | public class WebConfig extends WebMvcConfigurerAdapter { 30 | 31 | @Bean 32 | public TenantHandlerInterceptor tenantHandlerInterceptor() { 33 | return new TenantHandlerInterceptor(); 34 | } 35 | 36 | @Override 37 | public void addInterceptors(InterceptorRegistry registry) { 38 | registry.addInterceptor(tenantHandlerInterceptor()); 39 | super.addInterceptors(registry); 40 | } 41 | 42 | @Override 43 | public void configureMessageConverters(List> converters) { 44 | //Here we add our custom-configured HttpMessageConverter 45 | converters.add(jacksonMessageConverter()); 46 | super.configureMessageConverters(converters); 47 | } 48 | 49 | @Bean 50 | public MappingJackson2HttpMessageConverter jacksonMessageConverter() { 51 | MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(); 52 | messageConverter.setObjectMapper(objectMapper()); 53 | return messageConverter; 54 | } 55 | 56 | @Bean 57 | public ObjectMapper objectMapper(){ 58 | ObjectMapper mapper = new ObjectMapper(); 59 | //Registering Hibernate4Module to support lazy objects 60 | Hibernate4Module module = new Hibernate4Module(mapping()); 61 | module.enable(Hibernate4Module.Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS); 62 | mapper.registerModule(module); 63 | mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); 64 | return mapper; 65 | } 66 | 67 | @Bean 68 | public Mapping mapping() { 69 | //这里写使用硬编码的方式来获取id字段名,后续如需要再扩展实现 70 | Mapping mapping = new Mapping() { 71 | @Override 72 | public IdentifierGeneratorFactory getIdentifierGeneratorFactory() { 73 | return null; 74 | } 75 | 76 | @Override 77 | public Type getIdentifierType(String className) throws MappingException { 78 | return null; 79 | } 80 | 81 | @Override 82 | public String getIdentifierPropertyName(String className) throws MappingException { 83 | return "id"; 84 | } 85 | 86 | @Override 87 | public Type getReferencedPropertyType(String className, String propertyName) throws MappingException { 88 | return null; 89 | } 90 | }; 91 | 92 | return mapping; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/service/BaseService.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.service; 2 | 3 | import com.closer.common.constant.IDG; 4 | import com.closer.common.domain.BaseDomain; 5 | import com.closer.common.repository.BaseRepository; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.google.common.base.CaseFormat; 8 | import com.google.common.base.Converter; 9 | import org.hibernate.annotations.GenericGenerator; 10 | import org.springframework.beans.BeanUtils; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.transaction.annotation.Propagation; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import java.io.Serializable; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * BaseService 21 | * Created by closer on 2016/1/5. 22 | */ 23 | @Transactional 24 | public class BaseService, I extends Serializable> { 25 | 26 | @Autowired 27 | private BaseRepository repository; 28 | 29 | @Autowired 30 | private ObjectMapper objectMapper; 31 | 32 | public static final Converter CONVERTER 33 | = CaseFormat.LOWER_UNDERSCORE.converterTo(CaseFormat.LOWER_CAMEL); 34 | 35 | @Transactional(propagation = Propagation.SUPPORTS) 36 | public T findOne(I id) { 37 | return repository.findOne(id); 38 | } 39 | 40 | @Transactional(propagation = Propagation.SUPPORTS) 41 | public T findStrictOne(I id) { 42 | T t = findOne(id); 43 | if (t == null) { 44 | throw new RuntimeException("未找到指定id的对象"); 45 | } 46 | return t; 47 | } 48 | 49 | @Transactional(propagation = Propagation.SUPPORTS) 50 | public List findAll() { 51 | return repository.findAll(); 52 | } 53 | 54 | public T add(T t) { 55 | GenericGenerator generator = t.getClass().getAnnotation(GenericGenerator.class); 56 | if (!IDG.ASSIGNED.equals(generator.strategy())) { 57 | t.setId(null); 58 | } 59 | beforeAdd(t); 60 | return repository.save(t); 61 | } 62 | 63 | protected void beforeAdd(T t) { 64 | 65 | } 66 | 67 | /** 68 | * 尽量保护update方法,使得不被滥用,如果仅是修改部分字段时, 69 | * 应该在Service中定义相应的需求方法,底层调用该方法 70 | */ 71 | protected T update(T t) { 72 | if (t.getId() == null) { 73 | throw new RuntimeException("主键id不能为空"); 74 | } 75 | if (!exists(t.getId())) { 76 | throw new RuntimeException("数据不存在"); 77 | } 78 | return repository.save(t); 79 | } 80 | 81 | public T update(I id, Map map) { 82 | T oldDomain = findOne(id); 83 | if (oldDomain == null) { 84 | throw new RuntimeException("找不到相关对象"); 85 | } 86 | T newDomain = createDomainFromOldAndMap(oldDomain, map); 87 | beforeUpdate(oldDomain, newDomain); 88 | return update(newDomain); 89 | } 90 | 91 | private T createDomainFromOldAndMap(T oldDomain, Map map) { 92 | int size = map.entrySet().size(); 93 | String[] ignoreProperties = new String[size]; 94 | int i = 0; 95 | for (Map.Entry entry : map.entrySet()) { 96 | ignoreProperties[i] = CONVERTER.convert(entry.getKey()); 97 | i++; 98 | } 99 | T newDomain = (T) objectMapper.convertValue(map, oldDomain.getClass()); 100 | BeanUtils.copyProperties(oldDomain, newDomain, ignoreProperties); 101 | return newDomain; 102 | } 103 | 104 | protected void beforeUpdate(T oldDomain, T newDomain) { 105 | } 106 | 107 | public void delete(I id) { 108 | repository.delete(id); 109 | } 110 | 111 | protected void delete(T t) { 112 | beforeDelete(t); 113 | repository.delete(t); 114 | } 115 | 116 | protected void beforeDelete(T t) { 117 | } 118 | 119 | @Transactional(propagation = Propagation.SUPPORTS) 120 | public boolean exists(I id) { 121 | return repository.exists(id); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/config/RDMSConfig.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.config; 2 | 3 | import com.closer.common.handler.DynamicRoutingDataSource; 4 | import com.closer.common.handler.TableMapperInterceptor; 5 | import org.hibernate.dialect.Dialect; 6 | import org.hibernate.dialect.HSQLDialect; 7 | import org.hsqldb.jdbc.JDBCDataSource; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.ComponentScan; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 12 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 13 | import org.springframework.orm.jpa.JpaTransactionManager; 14 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 15 | import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; 16 | import org.springframework.stereotype.Repository; 17 | import org.springframework.transaction.PlatformTransactionManager; 18 | import org.springframework.transaction.annotation.EnableTransactionManagement; 19 | 20 | import javax.persistence.EntityManagerFactory; 21 | import javax.sql.DataSource; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | /** 26 | * 关系型数据库配置 27 | * Created by closer on 2016/1/3. 28 | */ 29 | @Configuration 30 | @EnableJpaRepositories(value = "com.closer", 31 | includeFilters = {@ComponentScan.Filter(Repository.class)}, 32 | enableDefaultTransactions = false) 33 | @EnableTransactionManagement(proxyTargetClass = true) 34 | @EnableJpaAuditing 35 | public class RDMSConfig { 36 | 37 | public static final Dialect DIALECT = new HSQLDialect(); 38 | private static final String INTERCEPTOR = TableMapperInterceptor.class.getCanonicalName(); 39 | 40 | @Bean 41 | public DataSource dataSource() { 42 | // EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); 43 | // return builder.setType(EmbeddedDatabaseType.HSQL).build(); 44 | //使用单独的HSQL服务 45 | // java -cp ../hsqldb-2.3.3.jar org.hsqldb.Server -database.0 testdb1 -dbname.0 testdbname1 -database.1 testdb2 -dbname.1 testdbname2 46 | // java -cp ../hsqldb-2.3.3.jar org.hsqldb.util.DatabaseManager -url jdbc:hsqldb:hsql://localhost/testdbname1 47 | JDBCDataSource dataSource1 = new JDBCDataSource(); 48 | dataSource1.setUrl("jdbc:hsqldb:hsql://localhost/testdbname1"); 49 | 50 | // java -cp ../hsqldb-2.3.3.jar org.hsqldb.util.DatabaseManager -url jdbc:hsqldb:hsql://localhost/testdbname2 51 | JDBCDataSource dataSource2 = new JDBCDataSource(); 52 | dataSource2.setUrl("jdbc:hsqldb:hsql://localhost/testdbname2"); 53 | 54 | Map targetDataSources = new HashMap<>(); 55 | targetDataSources.put("ds1", dataSource1); 56 | targetDataSources.put("ds2", dataSource2); 57 | 58 | DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(); 59 | dataSource.setTargetDataSources(targetDataSources); 60 | dataSource.setDefaultTargetDataSource(dataSource1); 61 | dataSource.afterPropertiesSet(); 62 | return dataSource; 63 | } 64 | 65 | 66 | @Bean 67 | public EntityManagerFactory entityManagerFactory() { 68 | HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); 69 | vendorAdapter.setGenerateDdl(true); 70 | vendorAdapter.setShowSql(true); 71 | 72 | LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); 73 | 74 | factory.setJpaVendorAdapter(vendorAdapter); 75 | factory.setPackagesToScan("com.closer..*.domain"); 76 | factory.setDataSource(dataSource()); 77 | factory.getJpaPropertyMap().put("hibernate.ejb.naming_strategy", "org.hibernate.cfg.ImprovedNamingStrategy"); 78 | factory.getJpaPropertyMap().put("hibernate.ejb.interceptor", INTERCEPTOR); 79 | //校验的模式 80 | // factory.setValidationMode(ValidationMode.NONE); 81 | factory.afterPropertiesSet(); 82 | return factory.getObject(); 83 | } 84 | 85 | @Bean 86 | public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { 87 | JpaTransactionManager txManager = new JpaTransactionManager(); 88 | txManager.setEntityManagerFactory(entityManagerFactory); 89 | //使用得@Transactional(propagation = Propagation.SUPPORTS)注解的方法可以真正不走事务 90 | txManager.setTransactionSynchronization(JpaTransactionManager.SYNCHRONIZATION_ON_ACTUAL_TRANSACTION); 91 | return txManager; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.config; 2 | 3 | import com.closer.tenant.domain.Tenant; 4 | import com.fasterxml.jackson.databind.JavaType; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.cache.CacheManager; 7 | import org.springframework.cache.annotation.EnableCaching; 8 | import org.springframework.cache.support.CompositeCacheManager; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.core.io.ClassPathResource; 12 | import org.springframework.data.redis.cache.RedisCacheManager; 13 | import org.springframework.data.redis.connection.RedisConnectionFactory; 14 | import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; 15 | import org.springframework.data.redis.core.RedisTemplate; 16 | import org.springframework.data.redis.core.StringRedisTemplate; 17 | import org.springframework.data.redis.core.script.DefaultRedisScript; 18 | import org.springframework.data.redis.core.script.RedisScript; 19 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 20 | import org.springframework.scripting.support.ResourceScriptSource; 21 | import redis.clients.jedis.JedisPoolConfig; 22 | 23 | import java.util.HashSet; 24 | import java.util.Set; 25 | 26 | /** 27 | * Redis配置 28 | * Created by Zhang Jinlong(150429) on 2016/1/25. 29 | */ 30 | @Configuration 31 | @EnableCaching 32 | public class RedisConfig { 33 | 34 | @Autowired 35 | private WebConfig webConfig; 36 | 37 | private static final String host = "localhost"; 38 | 39 | private static final int port = 6379; 40 | 41 | private static final int db = 0; 42 | 43 | private static final int maxIdle = 10; 44 | 45 | private static final int minIdle = 2; 46 | 47 | private static final int maxWait = 3000; 48 | 49 | private static final boolean testOnBorrow = true; 50 | 51 | @Bean 52 | public RedisConnectionFactory redisConnectionFactory() { 53 | JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); 54 | jedisPoolConfig.setMaxIdle(maxIdle); 55 | jedisPoolConfig.setMinIdle(minIdle); 56 | jedisPoolConfig.setMaxWaitMillis(maxWait); 57 | jedisPoolConfig.setTestOnBorrow(testOnBorrow); 58 | 59 | JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(); 60 | jedisConnectionFactory.setHostName(host); 61 | jedisConnectionFactory.setPort(port); 62 | jedisConnectionFactory.setDatabase(db); 63 | jedisConnectionFactory.setUsePool(true); 64 | jedisConnectionFactory.setPoolConfig(jedisPoolConfig); 65 | 66 | return jedisConnectionFactory; 67 | } 68 | 69 | @Bean 70 | public StringRedisTemplate redisTemplate() { 71 | StringRedisTemplate redisTemplate = new StringRedisTemplate(); 72 | redisTemplate.setConnectionFactory(redisConnectionFactory()); 73 | redisTemplate.afterPropertiesSet(); 74 | return redisTemplate; 75 | } 76 | 77 | @Bean 78 | public RedisScript idgScript() { 79 | DefaultRedisScript redisScript = new DefaultRedisScript<>(); 80 | redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("idg.lua"))); 81 | redisScript.setResultType(String.class); 82 | return redisScript; 83 | } 84 | 85 | @Bean 86 | public CacheManager cacheManager() { 87 | Set cacheManagers = new HashSet<>(); 88 | cacheManagers.add(getCacheManager(Tenant.class, "tenants")); 89 | CompositeCacheManager cacheManager = new CompositeCacheManager(); 90 | cacheManager.setCacheManagers(cacheManagers); 91 | cacheManager.afterPropertiesSet(); 92 | return cacheManager; 93 | } 94 | 95 | private CacheManager getCacheManager(Class type, String cacheName) { 96 | Jackson2JsonRedisSerializer redisSerializer = new Jackson2JsonRedisSerializer(type); 97 | return _getCacheManager(cacheName, redisSerializer); 98 | } 99 | 100 | private CacheManager getCacheManager(JavaType type, String cacheName) { 101 | Jackson2JsonRedisSerializer redisSerializer = new Jackson2JsonRedisSerializer(type); 102 | return _getCacheManager(cacheName, redisSerializer); 103 | } 104 | 105 | private CacheManager _getCacheManager(String cacheName, Jackson2JsonRedisSerializer redisSerializer) { 106 | redisSerializer.setObjectMapper(webConfig.objectMapper()); 107 | RedisTemplate redisTemplate = new RedisTemplate<>(); 108 | redisTemplate.setConnectionFactory(redisConnectionFactory()); 109 | redisTemplate.setDefaultSerializer(redisSerializer); 110 | redisTemplate.afterPropertiesSet(); 111 | 112 | Set cacheNames = new HashSet<>(); 113 | cacheNames.add(cacheName); 114 | RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate, cacheNames); 115 | cacheManager.setUsePrefix(true); 116 | //12 hours 117 | cacheManager.setDefaultExpiration(60 * 60 * 12); 118 | cacheManager.setTransactionAware(true); 119 | cacheManager.afterPropertiesSet(); 120 | return cacheManager; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/handler/TableHandler.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.handler; 2 | 3 | import com.closer.common.config.RDMSConfig; 4 | import com.closer.tenant.domain.Tenant; 5 | import com.closer.tenant.event.TenantCreateEvent; 6 | import com.closer.tenant.service.TenantSupport; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.hibernate.cfg.Configuration; 9 | import org.hibernate.cfg.ImprovedNamingStrategy; 10 | import org.hibernate.dialect.Dialect; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.context.ApplicationContext; 15 | import org.springframework.jdbc.datasource.DataSourceUtils; 16 | import org.springframework.scheduling.annotation.Async; 17 | import org.springframework.stereotype.Component; 18 | import org.springframework.transaction.event.TransactionalEventListener; 19 | 20 | import javax.annotation.PostConstruct; 21 | import javax.sql.DataSource; 22 | import java.sql.Connection; 23 | import java.sql.SQLException; 24 | import java.sql.Statement; 25 | import java.util.*; 26 | 27 | /** 28 | * 表处理帮助类 29 | * Created by closer on 2016/1/5. 30 | */ 31 | @Component 32 | public class TableHandler { 33 | 34 | 35 | private static final Logger LOG = LoggerFactory.getLogger(TableHandler.class); 36 | 37 | @Autowired 38 | private DataSource dataSource; 39 | 40 | @Autowired 41 | private List tenantSupports; 42 | 43 | @Autowired 44 | private ApplicationContext applicationContext; 45 | 46 | private Set entityClasses; 47 | 48 | @PostConstruct 49 | public void postConstruct() { 50 | entityClasses = new HashSet<>(); 51 | if (tenantSupports == null) { 52 | tenantSupports = Collections.emptyList(); 53 | return; 54 | } 55 | for (TenantSupport tenantSupport : tenantSupports) { 56 | entityClasses.addAll(Arrays.asList(tenantSupport.getEntities())); 57 | } 58 | } 59 | 60 | /** 61 | * 处理公司新增事件,该处理将于公司新增的保存提交后执行 62 | * 63 | * @param event 事件 64 | */ 65 | 66 | @TransactionalEventListener 67 | public void handleCompanyCreate(TenantCreateEvent event) { 68 | TableHandler self = applicationContext.getBean(TableHandler.class); 69 | self.createTable(entityClasses, event.getTenant()); 70 | } 71 | 72 | 73 | @Async 74 | public void createTable(Set entityClasses, Tenant tenant) { 75 | System.out.println(Thread.currentThread().getName()); 76 | TableProvider.setTenant(tenant); 77 | _createTable(entityClasses, TableProvider.PREFIX, TableProvider.getTablePrefix()); 78 | TableProvider.clear(); 79 | } 80 | 81 | 82 | private void _createTable(Set entityClasses, String... keyValues) { 83 | if (keyValues.length % 2 == 1) { 84 | throw new RuntimeException("创建表时需要替换的字符与值应成对出现"); 85 | } 86 | 87 | String[] sqlArr = entity2Sql(RDMSConfig.DIALECT, entityClasses); 88 | 89 | Connection connection = DataSourceUtils.getConnection(dataSource); 90 | try (Statement stmt = connection.createStatement()) { 91 | for (String sql : sqlArr) { 92 | for (int i = 0; i < keyValues.length; i = i + 2) { 93 | String key = keyValues[i]; 94 | String value = keyValues[i + 1]; 95 | sql = sql.replace(key, value); 96 | } 97 | sql = make(sql); 98 | LOG.info("建表sql:{}", sql); 99 | executeSql(stmt, sql); 100 | } 101 | } catch (SQLException e) { 102 | String msg = "创建表失败"; 103 | throw new RuntimeException(msg, e); 104 | } finally { 105 | DataSourceUtils.releaseConnection(connection, dataSource); 106 | } 107 | } 108 | 109 | private static void executeSql(Statement stmt, String sql) { 110 | try { 111 | stmt.execute(sql); 112 | } catch (SQLException e) { 113 | if (!sql.startsWith("alter table")) { 114 | String msg = "创建表失败"; 115 | throw new RuntimeException(msg, e); 116 | } 117 | LOG.warn(e.getMessage()); 118 | } 119 | } 120 | 121 | /** 122 | * 对sql进行特殊处理 123 | */ 124 | private static String make(String sql) { 125 | //增加if not exists 126 | if (sql.startsWith("create table")) { 127 | return sql.replace("create table", "create table if not exists"); 128 | } 129 | //重命名约束名 130 | if (sql.contains("add constraint")) { 131 | String[] fields = sql.split("\\s"); 132 | fields[5] = fields[5] + TableProvider.getTableNum(); 133 | return StringUtils.join(fields, " "); 134 | } 135 | return sql; 136 | } 137 | 138 | private String[] entity2Sql(Dialect dialect, Set entityClasses) { 139 | Configuration cfg = new Configuration(); 140 | cfg.setNamingStrategy(new ImprovedNamingStrategy()); 141 | for (Class entityClass : entityClasses) { 142 | cfg.addAnnotatedClass(entityClass); 143 | } 144 | return cfg.generateSchemaCreationScript(dialect); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/init-sql/hsqldb/quartz-init.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- In your Quartz properties file, you'll need to set 3 | -- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.HSQLDBDelegate 4 | -- 注意:这里把官方的BINARY类型全部替换成了LONGVARBINARY,否则会报错 5 | -- 6 | 7 | DROP TABLE qrtz_locks IF EXISTS; 8 | DROP TABLE qrtz_scheduler_state IF EXISTS; 9 | DROP TABLE qrtz_fired_triggers IF EXISTS; 10 | DROP TABLE qrtz_paused_trigger_grps IF EXISTS; 11 | DROP TABLE qrtz_calendars IF EXISTS; 12 | DROP TABLE qrtz_blob_triggers IF EXISTS; 13 | DROP TABLE qrtz_cron_triggers IF EXISTS; 14 | DROP TABLE qrtz_simple_triggers IF EXISTS; 15 | DROP TABLE qrtz_simprop_triggers IF EXISTS; 16 | DROP TABLE qrtz_triggers IF EXISTS; 17 | DROP TABLE qrtz_job_details IF EXISTS; 18 | 19 | CREATE TABLE qrtz_job_details 20 | ( 21 | SCHED_NAME VARCHAR(120) NOT NULL, 22 | JOB_NAME VARCHAR(200) NOT NULL, 23 | JOB_GROUP VARCHAR(200) NOT NULL, 24 | DESCRIPTION VARCHAR(250) NULL, 25 | JOB_CLASS_NAME VARCHAR(250) NOT NULL, 26 | IS_DURABLE BOOLEAN NOT NULL, 27 | IS_NONCONCURRENT BOOLEAN NOT NULL, 28 | IS_UPDATE_DATA BOOLEAN NOT NULL, 29 | REQUESTS_RECOVERY BOOLEAN NOT NULL, 30 | JOB_DATA LONGVARBINARY NULL, 31 | PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) 32 | ); 33 | 34 | CREATE TABLE qrtz_triggers 35 | ( 36 | SCHED_NAME VARCHAR(120) NOT NULL, 37 | TRIGGER_NAME VARCHAR(200) NOT NULL, 38 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 39 | JOB_NAME VARCHAR(200) NOT NULL, 40 | JOB_GROUP VARCHAR(200) NOT NULL, 41 | DESCRIPTION VARCHAR(250) NULL, 42 | NEXT_FIRE_TIME NUMERIC(13) NULL, 43 | PREV_FIRE_TIME NUMERIC(13) NULL, 44 | PRIORITY INTEGER NULL, 45 | TRIGGER_STATE VARCHAR(16) NOT NULL, 46 | TRIGGER_TYPE VARCHAR(8) NOT NULL, 47 | START_TIME NUMERIC(13) NOT NULL, 48 | END_TIME NUMERIC(13) NULL, 49 | CALENDAR_NAME VARCHAR(200) NULL, 50 | MISFIRE_INSTR NUMERIC(2) NULL, 51 | JOB_DATA LONGVARBINARY NULL, 52 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 53 | FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) 54 | REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) 55 | ); 56 | 57 | CREATE TABLE qrtz_simple_triggers 58 | ( 59 | SCHED_NAME VARCHAR(120) NOT NULL, 60 | TRIGGER_NAME VARCHAR(200) NOT NULL, 61 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 62 | REPEAT_COUNT NUMERIC(7) NOT NULL, 63 | REPEAT_INTERVAL NUMERIC(12) NOT NULL, 64 | TIMES_TRIGGERED NUMERIC(10) NOT NULL, 65 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 66 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 67 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 68 | ); 69 | 70 | CREATE TABLE qrtz_cron_triggers 71 | ( 72 | SCHED_NAME VARCHAR(120) NOT NULL, 73 | TRIGGER_NAME VARCHAR(200) NOT NULL, 74 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 75 | CRON_EXPRESSION VARCHAR(120) NOT NULL, 76 | TIME_ZONE_ID VARCHAR(80), 77 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 78 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 79 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 80 | ); 81 | 82 | CREATE TABLE qrtz_simprop_triggers 83 | ( 84 | SCHED_NAME VARCHAR(120) NOT NULL, 85 | TRIGGER_NAME VARCHAR(200) NOT NULL, 86 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 87 | STR_PROP_1 VARCHAR(512) NULL, 88 | STR_PROP_2 VARCHAR(512) NULL, 89 | STR_PROP_3 VARCHAR(512) NULL, 90 | INT_PROP_1 NUMERIC(9) NULL, 91 | INT_PROP_2 NUMERIC(9) NULL, 92 | LONG_PROP_1 NUMERIC(13) NULL, 93 | LONG_PROP_2 NUMERIC(13) NULL, 94 | DEC_PROP_1 NUMERIC(13,4) NULL, 95 | DEC_PROP_2 NUMERIC(13,4) NULL, 96 | BOOL_PROP_1 BOOLEAN NULL, 97 | BOOL_PROP_2 BOOLEAN NULL, 98 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 99 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 100 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 101 | ); 102 | 103 | CREATE TABLE qrtz_blob_triggers 104 | ( 105 | SCHED_NAME VARCHAR(120) NOT NULL, 106 | TRIGGER_NAME VARCHAR(200) NOT NULL, 107 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 108 | BLOB_DATA LONGVARBINARY NULL, 109 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 110 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 111 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 112 | ); 113 | 114 | CREATE TABLE qrtz_calendars 115 | ( 116 | SCHED_NAME VARCHAR(120) NOT NULL, 117 | CALENDAR_NAME VARCHAR(200) NOT NULL, 118 | CALENDAR LONGVARBINARY NOT NULL, 119 | PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) 120 | ); 121 | 122 | CREATE TABLE qrtz_paused_trigger_grps 123 | ( 124 | SCHED_NAME VARCHAR(120) NOT NULL, 125 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 126 | PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) 127 | ); 128 | 129 | CREATE TABLE qrtz_fired_triggers 130 | ( 131 | SCHED_NAME VARCHAR(120) NOT NULL, 132 | ENTRY_ID VARCHAR(95) NOT NULL, 133 | TRIGGER_NAME VARCHAR(200) NOT NULL, 134 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 135 | INSTANCE_NAME VARCHAR(200) NOT NULL, 136 | FIRED_TIME NUMERIC(13) NOT NULL, 137 | SCHED_TIME NUMERIC(13) NOT NULL, 138 | PRIORITY INTEGER NOT NULL, 139 | STATE VARCHAR(16) NOT NULL, 140 | JOB_NAME VARCHAR(200) NULL, 141 | JOB_GROUP VARCHAR(200) NULL, 142 | IS_NONCONCURRENT BOOLEAN NULL, 143 | REQUESTS_RECOVERY BOOLEAN NULL, 144 | PRIMARY KEY (SCHED_NAME,ENTRY_ID) 145 | ); 146 | 147 | CREATE TABLE qrtz_scheduler_state 148 | ( 149 | SCHED_NAME VARCHAR(120) NOT NULL, 150 | INSTANCE_NAME VARCHAR(200) NOT NULL, 151 | LAST_CHECKIN_TIME NUMERIC(13) NOT NULL, 152 | CHECKIN_INTERVAL NUMERIC(13) NOT NULL, 153 | PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) 154 | ); 155 | 156 | CREATE TABLE qrtz_locks 157 | ( 158 | SCHED_NAME VARCHAR(120) NOT NULL, 159 | LOCK_NAME VARCHAR(40) NOT NULL, 160 | PRIMARY KEY (SCHED_NAME,LOCK_NAME) 161 | ); -------------------------------------------------------------------------------- /src/main/init-sql/mysql/tables_mysql.sql: -------------------------------------------------------------------------------- 1 | # 2 | # Quartz seems to work best with the driver mm.mysql-2.0.7-bin.jar 3 | # 4 | # PLEASE consider using mysql with innodb tables to avoid locking issues 5 | # 6 | # In your Quartz properties file, you'll need to set 7 | # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate 8 | # 9 | 10 | DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; 11 | DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; 12 | DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; 13 | DROP TABLE IF EXISTS QRTZ_LOCKS; 14 | DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; 15 | DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; 16 | DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; 17 | DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; 18 | DROP TABLE IF EXISTS QRTZ_TRIGGERS; 19 | DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; 20 | DROP TABLE IF EXISTS QRTZ_CALENDARS; 21 | 22 | 23 | CREATE TABLE QRTZ_JOB_DETAILS 24 | ( 25 | SCHED_NAME VARCHAR(120) NOT NULL, 26 | JOB_NAME VARCHAR(200) NOT NULL, 27 | JOB_GROUP VARCHAR(200) NOT NULL, 28 | DESCRIPTION VARCHAR(250) NULL, 29 | JOB_CLASS_NAME VARCHAR(250) NOT NULL, 30 | IS_DURABLE VARCHAR(1) NOT NULL, 31 | IS_NONCONCURRENT VARCHAR(1) NOT NULL, 32 | IS_UPDATE_DATA VARCHAR(1) NOT NULL, 33 | REQUESTS_RECOVERY VARCHAR(1) NOT NULL, 34 | JOB_DATA BLOB NULL, 35 | PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) 36 | ); 37 | 38 | CREATE TABLE QRTZ_TRIGGERS 39 | ( 40 | SCHED_NAME VARCHAR(120) NOT NULL, 41 | TRIGGER_NAME VARCHAR(200) NOT NULL, 42 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 43 | JOB_NAME VARCHAR(200) NOT NULL, 44 | JOB_GROUP VARCHAR(200) NOT NULL, 45 | DESCRIPTION VARCHAR(250) NULL, 46 | NEXT_FIRE_TIME BIGINT(13) NULL, 47 | PREV_FIRE_TIME BIGINT(13) NULL, 48 | PRIORITY INTEGER NULL, 49 | TRIGGER_STATE VARCHAR(16) NOT NULL, 50 | TRIGGER_TYPE VARCHAR(8) NOT NULL, 51 | START_TIME BIGINT(13) NOT NULL, 52 | END_TIME BIGINT(13) NULL, 53 | CALENDAR_NAME VARCHAR(200) NULL, 54 | MISFIRE_INSTR SMALLINT(2) NULL, 55 | JOB_DATA BLOB NULL, 56 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 57 | FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) 58 | REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) 59 | ); 60 | 61 | CREATE TABLE QRTZ_SIMPLE_TRIGGERS 62 | ( 63 | SCHED_NAME VARCHAR(120) NOT NULL, 64 | TRIGGER_NAME VARCHAR(200) NOT NULL, 65 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 66 | REPEAT_COUNT BIGINT(7) NOT NULL, 67 | REPEAT_INTERVAL BIGINT(12) NOT NULL, 68 | TIMES_TRIGGERED BIGINT(10) NOT NULL, 69 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 70 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 71 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 72 | ); 73 | 74 | CREATE TABLE QRTZ_CRON_TRIGGERS 75 | ( 76 | SCHED_NAME VARCHAR(120) NOT NULL, 77 | TRIGGER_NAME VARCHAR(200) NOT NULL, 78 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 79 | CRON_EXPRESSION VARCHAR(200) NOT NULL, 80 | TIME_ZONE_ID VARCHAR(80), 81 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 82 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 83 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 84 | ); 85 | 86 | CREATE TABLE QRTZ_SIMPROP_TRIGGERS 87 | ( 88 | SCHED_NAME VARCHAR(120) NOT NULL, 89 | TRIGGER_NAME VARCHAR(200) NOT NULL, 90 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 91 | STR_PROP_1 VARCHAR(512) NULL, 92 | STR_PROP_2 VARCHAR(512) NULL, 93 | STR_PROP_3 VARCHAR(512) NULL, 94 | INT_PROP_1 INT NULL, 95 | INT_PROP_2 INT NULL, 96 | LONG_PROP_1 BIGINT NULL, 97 | LONG_PROP_2 BIGINT NULL, 98 | DEC_PROP_1 NUMERIC(13,4) NULL, 99 | DEC_PROP_2 NUMERIC(13,4) NULL, 100 | BOOL_PROP_1 VARCHAR(1) NULL, 101 | BOOL_PROP_2 VARCHAR(1) NULL, 102 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 103 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 104 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 105 | ); 106 | 107 | CREATE TABLE QRTZ_BLOB_TRIGGERS 108 | ( 109 | SCHED_NAME VARCHAR(120) NOT NULL, 110 | TRIGGER_NAME VARCHAR(200) NOT NULL, 111 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 112 | BLOB_DATA BLOB NULL, 113 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 114 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 115 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 116 | ); 117 | 118 | CREATE TABLE QRTZ_CALENDARS 119 | ( 120 | SCHED_NAME VARCHAR(120) NOT NULL, 121 | CALENDAR_NAME VARCHAR(200) NOT NULL, 122 | CALENDAR BLOB NOT NULL, 123 | PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) 124 | ); 125 | 126 | CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS 127 | ( 128 | SCHED_NAME VARCHAR(120) NOT NULL, 129 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 130 | PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) 131 | ); 132 | 133 | CREATE TABLE QRTZ_FIRED_TRIGGERS 134 | ( 135 | SCHED_NAME VARCHAR(120) NOT NULL, 136 | ENTRY_ID VARCHAR(95) NOT NULL, 137 | TRIGGER_NAME VARCHAR(200) NOT NULL, 138 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 139 | INSTANCE_NAME VARCHAR(200) NOT NULL, 140 | FIRED_TIME BIGINT(13) NOT NULL, 141 | SCHED_TIME BIGINT(13) NOT NULL, 142 | PRIORITY INTEGER NOT NULL, 143 | STATE VARCHAR(16) NOT NULL, 144 | JOB_NAME VARCHAR(200) NULL, 145 | JOB_GROUP VARCHAR(200) NULL, 146 | IS_NONCONCURRENT VARCHAR(1) NULL, 147 | REQUESTS_RECOVERY VARCHAR(1) NULL, 148 | PRIMARY KEY (SCHED_NAME,ENTRY_ID) 149 | ); 150 | 151 | CREATE TABLE QRTZ_SCHEDULER_STATE 152 | ( 153 | SCHED_NAME VARCHAR(120) NOT NULL, 154 | INSTANCE_NAME VARCHAR(200) NOT NULL, 155 | LAST_CHECKIN_TIME BIGINT(13) NOT NULL, 156 | CHECKIN_INTERVAL BIGINT(13) NOT NULL, 157 | PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) 158 | ); 159 | 160 | CREATE TABLE QRTZ_LOCKS 161 | ( 162 | SCHED_NAME VARCHAR(120) NOT NULL, 163 | LOCK_NAME VARCHAR(40) NOT NULL, 164 | PRIMARY KEY (SCHED_NAME,LOCK_NAME) 165 | ); 166 | 167 | 168 | commit; 169 | -------------------------------------------------------------------------------- /src/main/java/com/closer/common/handler/DistributedIdentifierGenerator.java: -------------------------------------------------------------------------------- 1 | package com.closer.common.handler; 2 | 3 | import com.closer.common.helper.ContextHelper; 4 | import org.hibernate.HibernateException; 5 | import org.hibernate.MappingException; 6 | import org.hibernate.dialect.Dialect; 7 | import org.hibernate.engine.spi.SessionImplementor; 8 | import org.hibernate.id.Configurable; 9 | import org.hibernate.id.IdentifierGenerator; 10 | import org.hibernate.type.Type; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.data.redis.core.RedisTemplate; 14 | import org.springframework.data.redis.core.StringRedisTemplate; 15 | import org.springframework.data.redis.core.script.RedisScript; 16 | 17 | import java.io.Serializable; 18 | import java.util.Collections; 19 | import java.util.Date; 20 | import java.util.Properties; 21 | 22 | /** 23 | * 支持分布式 24 | * 规则: 25 | * Long类型,总计占53位(考虑JavaScript仅能表示53位整形) 26 | * 1.时间秒数,占 30 bit,可表示[34]年 27 | * 2.集群间计数器 占 {COUNTER_BITS} bit,可表示[1024]个数,这里使用redis来处理,每过{PER_TIME}毫秒进行清0 28 | * 3.实例内计数器 占 {SEQUENCE_BITS} bit,可表示[8096]个数,每过{COUNTER_EXPIRE_TIME}毫秒进行清0 29 | * 注意:这里的{COUNTER_EXPIRE_TIME}不宜设置过大,过大之后,当redis宕掉恢复后,如果计数又重新开始,且又 30 | * 在同一个{COUNTER_EXPIRE_TIME}时间窗口内,则会引起主键重复。同时又不宜设置过小,会导致频繁的读写redis。 31 | * 这里主要考虑的是一个{PER_TIME}时间窗口内,redis宕掉之后也无法恢复。 32 | * 实现参考:http://www.oschina.net/code/snippet_147955_25122 33 | * Created by closer on 2016/2/2. 34 | * @since 1.0 35 | */ 36 | //TODO 还需要做性能测试 37 | public class DistributedIdentifierGenerator implements IdentifierGenerator, Configurable { 38 | 39 | private static final Logger LOG = LoggerFactory.getLogger(DistributedIdentifierGenerator.class); 40 | 41 | /** 42 | * 项目起始纪元(此处取2016-01-01:00:00:00.000) 43 | */ 44 | private static final long PROJECT_EPOCH = 1451577600L; 45 | 46 | /** 47 | * 集群间计数过期时间,单位秒 48 | */ 49 | private static final long COUNTER_EXPIRE_TIME = 60 * 5L; 50 | 51 | /** 52 | * 集群间计数所占位数 53 | */ 54 | private static final int COUNTER_BITS = 10; 55 | /** 56 | * 实例内计数所占位数 57 | */ 58 | private static final int SEQUENCE_BITS = 13; 59 | 60 | /** 61 | * 时间位移数 62 | */ 63 | private static final int TIME_SHIFT = COUNTER_BITS + SEQUENCE_BITS; 64 | 65 | /** 66 | * 集群间计数位移数 67 | */ 68 | private static final int COUNTER_SHIFT = SEQUENCE_BITS; 69 | 70 | private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); 71 | 72 | private static final long COUNTER_MASK = ~(-1L << COUNTER_BITS); 73 | 74 | private RedisTemplate redisTemplate; 75 | private RedisScript script; 76 | 77 | private long lastTimestamp = -1L; 78 | 79 | private long sequence = 0L; 80 | 81 | private long counter = 0L; 82 | 83 | private long counterRefreshTimestamp = -1L; 84 | 85 | private String key; 86 | 87 | @Override 88 | public void configure(Type type, Properties params, Dialect d) throws MappingException { 89 | String jpaEntityName = params.getProperty(IdentifierGenerator.JPA_ENTITY_NAME); 90 | key = "idg:" + jpaEntityName; 91 | } 92 | 93 | @Override 94 | public Serializable generate(SessionImplementor session, Object object) throws HibernateException { 95 | return nextId(); 96 | } 97 | 98 | /** 99 | * 获取集群间计数 100 | * 101 | * @return 集群间计数 102 | */ 103 | private long getDistributedCounter(long timestamp) { 104 | if (redisTemplate == null) { 105 | synchronized (this) { 106 | if (redisTemplate == null) { 107 | redisTemplate = ContextHelper.applicationContext() 108 | .getBean("redisTemplate", StringRedisTemplate.class); 109 | script = (RedisScript) ContextHelper.applicationContext().getBean("idgScript"); 110 | } 111 | } 112 | } 113 | 114 | String resultStr = redisTemplate.execute(script, Collections.EMPTY_LIST, 115 | key, String.valueOf(COUNTER_EXPIRE_TIME), String.valueOf(COUNTER_MASK), 116 | String.valueOf(COUNTER_BITS)); 117 | String[] result = resultStr.split(","); 118 | counterRefreshTimestamp = timestamp + Long.parseLong(result[0]); 119 | return Long.parseLong(result[1]) & COUNTER_MASK; 120 | } 121 | 122 | private synchronized long nextId() { 123 | long timestamp = timeGen(); 124 | if (timestamp < lastTimestamp) { 125 | LOG.error(String.format("时间回退了. 拒绝直到%d的请求", lastTimestamp)); 126 | throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); 127 | } 128 | 129 | if (timestamp == lastTimestamp) { 130 | sequence = (sequence + 1) & SEQUENCE_MASK; 131 | if (sequence == 0) { 132 | timestamp = tilNextSecond(lastTimestamp); 133 | } 134 | } else { 135 | sequence = 0L; 136 | } 137 | if (timestamp > counterRefreshTimestamp) { 138 | counter = getDistributedCounter(timestamp); 139 | } 140 | 141 | lastTimestamp = timestamp; 142 | 143 | return ((timestamp - PROJECT_EPOCH) << TIME_SHIFT) | (counter << COUNTER_SHIFT) | sequence; 144 | } 145 | 146 | private long tilNextSecond(long lastTimestamp) { 147 | long timestamp = timeGen(); 148 | while (timestamp == lastTimestamp) { 149 | timestamp = timeGen(); 150 | } 151 | return timestamp; 152 | } 153 | 154 | private long timeGen() { 155 | return System.currentTimeMillis() / 1000; 156 | } 157 | 158 | public static void main(String[] args) { 159 | long add = (1L << 30) * 1000; 160 | System.out.println(new Date(PROJECT_EPOCH * 1000 + add)); 161 | 162 | long id = 30784723361792L; 163 | long time = ((id >> TIME_SHIFT) + PROJECT_EPOCH) * 1000; 164 | System.out.println(new Date(time)); 165 | 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /dynamic-table-mapper-postman.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ba9067a6-d7e5-0cee-7db8-c21ff3201602", 3 | "name": "dynamic-table-mapper", 4 | "description": "", 5 | "order": [], 6 | "folders": [ 7 | { 8 | "id": "c8f96c93-5f25-915e-ce22-20daaee08427", 9 | "name": "员工", 10 | "description": "", 11 | "order": [ 12 | "d548fc14-fcfb-f14b-36e2-a403556f0720", 13 | "5b135903-cd04-f2f5-220e-dd6e89371a96", 14 | "0c9c6222-0d81-13b0-9678-992fdc265a79" 15 | ], 16 | "owner": "62404" 17 | }, 18 | { 19 | "id": "c5cf1c00-1f4b-7cae-96c9-b9ce54fce0d9", 20 | "name": "租户(公司)", 21 | "description": "", 22 | "order": [ 23 | "e2ad2188-654d-a895-1697-3a61a2b4d2de", 24 | "1a1deca1-d68d-fb09-3295-b51d53cfce1e" 25 | ], 26 | "owner": "62404" 27 | }, 28 | { 29 | "id": "211d3f14-a301-8515-a9b4-17445b717472", 30 | "name": "部门", 31 | "description": "", 32 | "order": [ 33 | "fb876cc1-dcf3-2396-6fbc-fef8bfc3a278", 34 | "38472113-04ad-4f85-3d6b-6af54655cae3", 35 | "7d73e433-9a3d-8ee1-2894-72d6098db7b0" 36 | ], 37 | "owner": "62404" 38 | } 39 | ], 40 | "timestamp": 1452087127684, 41 | "owner": "62404", 42 | "remoteLink": "", 43 | "public": false, 44 | "requests": [ 45 | { 46 | "id": "0c9c6222-0d81-13b0-9678-992fdc265a79", 47 | "headers": "Content-Type: application/json;charset=utf-8\ntenant: 1\n", 48 | "url": "{{HOST}}/employees", 49 | "preRequestScript": "", 50 | "pathVariables": {}, 51 | "method": "POST", 52 | "data": [], 53 | "dataMode": "raw", 54 | "version": 2, 55 | "tests": "", 56 | "currentHelper": "normal", 57 | "helperAttributes": {}, 58 | "time": 1458208674123, 59 | "name": "员工-新增", 60 | "description": "", 61 | "collectionId": "ba9067a6-d7e5-0cee-7db8-c21ff3201602", 62 | "responses": [], 63 | "rawModeData": "{\n \"name\":\"阿猫\",\n \"department\":{\"id\":55624607997952}\n}" 64 | }, 65 | { 66 | "id": "1a1deca1-d68d-fb09-3295-b51d53cfce1e", 67 | "headers": "Content-Type: application/json;charset=utf-8\n", 68 | "url": "{{HOST}}/tenants/1", 69 | "preRequestScript": "", 70 | "pathVariables": {}, 71 | "method": "GET", 72 | "data": [], 73 | "dataMode": "params", 74 | "version": 2, 75 | "tests": "", 76 | "currentHelper": "normal", 77 | "helperAttributes": "{}", 78 | "time": 1453950816436, 79 | "name": "租户-详情", 80 | "description": "", 81 | "collectionId": "ba9067a6-d7e5-0cee-7db8-c21ff3201602", 82 | "responses": [], 83 | "folder": "c5cf1c00-1f4b-7cae-96c9-b9ce54fce0d9", 84 | "isFromCollection": true, 85 | "collectionRequestId": "1a1deca1-d68d-fb09-3295-b51d53cfce1e" 86 | }, 87 | { 88 | "id": "38472113-04ad-4f85-3d6b-6af54655cae3", 89 | "headers": "Content-Type: application/json;charset=utf-8\ntenant: 1\n", 90 | "url": "{{HOST}}/departments", 91 | "preRequestScript": "", 92 | "pathVariables": {}, 93 | "method": "POST", 94 | "data": [], 95 | "dataMode": "raw", 96 | "version": 2, 97 | "tests": "", 98 | "currentHelper": "normal", 99 | "helperAttributes": {}, 100 | "time": 1458208090828, 101 | "name": "部门-新增", 102 | "description": "", 103 | "collectionId": "ba9067a6-d7e5-0cee-7db8-c21ff3201602", 104 | "responses": [], 105 | "rawModeData": "{\n \"name\":\"事业部\"\n}" 106 | }, 107 | { 108 | "id": "5b135903-cd04-f2f5-220e-dd6e89371a96", 109 | "headers": "tenant: 1\n", 110 | "url": "{{HOST}}/employees/55625480413184", 111 | "preRequestScript": "", 112 | "pathVariables": {}, 113 | "method": "GET", 114 | "data": [], 115 | "dataMode": "params", 116 | "version": 2, 117 | "tests": "", 118 | "currentHelper": "normal", 119 | "helperAttributes": {}, 120 | "time": 1458208710899, 121 | "name": "员工-详情", 122 | "description": "", 123 | "collectionId": "ba9067a6-d7e5-0cee-7db8-c21ff3201602", 124 | "responses": [] 125 | }, 126 | { 127 | "id": "7d73e433-9a3d-8ee1-2894-72d6098db7b0", 128 | "headers": "tenant: 1\n", 129 | "url": "{{HOST}}/departments/55624607997952", 130 | "preRequestScript": "", 131 | "pathVariables": {}, 132 | "method": "GET", 133 | "data": [], 134 | "dataMode": "params", 135 | "version": 2, 136 | "tests": "", 137 | "currentHelper": "normal", 138 | "helperAttributes": {}, 139 | "time": 1458208643259, 140 | "name": "部门-详情", 141 | "description": "", 142 | "collectionId": "ba9067a6-d7e5-0cee-7db8-c21ff3201602", 143 | "responses": [] 144 | }, 145 | { 146 | "id": "d548fc14-fcfb-f14b-36e2-a403556f0720", 147 | "headers": "tenant: 1\n", 148 | "url": "{{HOST}}/employees", 149 | "preRequestScript": "", 150 | "pathVariables": {}, 151 | "method": "GET", 152 | "data": [], 153 | "dataMode": "params", 154 | "version": 2, 155 | "tests": "", 156 | "currentHelper": "normal", 157 | "helperAttributes": {}, 158 | "time": 1458208024429, 159 | "name": "员工-列表", 160 | "description": "", 161 | "collectionId": "ba9067a6-d7e5-0cee-7db8-c21ff3201602", 162 | "responses": [] 163 | }, 164 | { 165 | "id": "e2ad2188-654d-a895-1697-3a61a2b4d2de", 166 | "headers": "Content-Type: application/json;charset=utf-8\n", 167 | "url": "{{HOST}}/tenants", 168 | "preRequestScript": "", 169 | "pathVariables": {}, 170 | "method": "POST", 171 | "data": [], 172 | "dataMode": "raw", 173 | "version": 2, 174 | "tests": "", 175 | "currentHelper": "normal", 176 | "helperAttributes": {}, 177 | "time": 1458208001153, 178 | "name": "租户-新增", 179 | "description": "", 180 | "collectionId": "ba9067a6-d7e5-0cee-7db8-c21ff3201602", 181 | "responses": [], 182 | "rawModeData": "{\n \"name\":\"网龙\",\n \"data_source_name\":\"ds2\"\n}" 183 | }, 184 | { 185 | "id": "fb876cc1-dcf3-2396-6fbc-fef8bfc3a278", 186 | "headers": "tenant: 1\n", 187 | "url": "{{HOST}}/departments", 188 | "preRequestScript": "", 189 | "pathVariables": {}, 190 | "method": "GET", 191 | "data": [], 192 | "dataMode": "params", 193 | "version": 2, 194 | "tests": "", 195 | "currentHelper": "normal", 196 | "helperAttributes": {}, 197 | "time": 1458208171563, 198 | "name": "部门-列表", 199 | "description": "", 200 | "collectionId": "ba9067a6-d7e5-0cee-7db8-c21ff3201602", 201 | "responses": [] 202 | } 203 | ] 204 | } -------------------------------------------------------------------------------- /src/main/init-sql/mysql/tables_mysql_innodb.sql: -------------------------------------------------------------------------------- 1 | # 2 | # In your Quartz properties file, you'll need to set 3 | # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate 4 | # 5 | # 6 | # By: Ron Cordell - roncordell 7 | # I didn't see this anywhere, so I thought I'd post it here. This is the script from Quartz to create the tables in a MySQL database, modified to use INNODB instead of MYISAM. 8 | 9 | DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; 10 | DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; 11 | DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; 12 | DROP TABLE IF EXISTS QRTZ_LOCKS; 13 | DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; 14 | DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; 15 | DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; 16 | DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; 17 | DROP TABLE IF EXISTS QRTZ_TRIGGERS; 18 | DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; 19 | DROP TABLE IF EXISTS QRTZ_CALENDARS; 20 | 21 | CREATE TABLE QRTZ_JOB_DETAILS( 22 | SCHED_NAME VARCHAR(120) NOT NULL, 23 | JOB_NAME VARCHAR(200) NOT NULL, 24 | JOB_GROUP VARCHAR(200) NOT NULL, 25 | DESCRIPTION VARCHAR(250) NULL, 26 | JOB_CLASS_NAME VARCHAR(250) NOT NULL, 27 | IS_DURABLE VARCHAR(1) NOT NULL, 28 | IS_NONCONCURRENT VARCHAR(1) NOT NULL, 29 | IS_UPDATE_DATA VARCHAR(1) NOT NULL, 30 | REQUESTS_RECOVERY VARCHAR(1) NOT NULL, 31 | JOB_DATA BLOB NULL, 32 | PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)) 33 | ENGINE=InnoDB; 34 | 35 | CREATE TABLE QRTZ_TRIGGERS ( 36 | SCHED_NAME VARCHAR(120) NOT NULL, 37 | TRIGGER_NAME VARCHAR(200) NOT NULL, 38 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 39 | JOB_NAME VARCHAR(200) NOT NULL, 40 | JOB_GROUP VARCHAR(200) NOT NULL, 41 | DESCRIPTION VARCHAR(250) NULL, 42 | NEXT_FIRE_TIME BIGINT(13) NULL, 43 | PREV_FIRE_TIME BIGINT(13) NULL, 44 | PRIORITY INTEGER NULL, 45 | TRIGGER_STATE VARCHAR(16) NOT NULL, 46 | TRIGGER_TYPE VARCHAR(8) NOT NULL, 47 | START_TIME BIGINT(13) NOT NULL, 48 | END_TIME BIGINT(13) NULL, 49 | CALENDAR_NAME VARCHAR(200) NULL, 50 | MISFIRE_INSTR SMALLINT(2) NULL, 51 | JOB_DATA BLOB NULL, 52 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 53 | FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) 54 | REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)) 55 | ENGINE=InnoDB; 56 | 57 | CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( 58 | SCHED_NAME VARCHAR(120) NOT NULL, 59 | TRIGGER_NAME VARCHAR(200) NOT NULL, 60 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 61 | REPEAT_COUNT BIGINT(7) NOT NULL, 62 | REPEAT_INTERVAL BIGINT(12) NOT NULL, 63 | TIMES_TRIGGERED BIGINT(10) NOT NULL, 64 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 65 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 66 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) 67 | ENGINE=InnoDB; 68 | 69 | CREATE TABLE QRTZ_CRON_TRIGGERS ( 70 | SCHED_NAME VARCHAR(120) NOT NULL, 71 | TRIGGER_NAME VARCHAR(200) NOT NULL, 72 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 73 | CRON_EXPRESSION VARCHAR(120) NOT NULL, 74 | TIME_ZONE_ID VARCHAR(80), 75 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 76 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 77 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) 78 | ENGINE=InnoDB; 79 | 80 | CREATE TABLE QRTZ_SIMPROP_TRIGGERS 81 | ( 82 | SCHED_NAME VARCHAR(120) NOT NULL, 83 | TRIGGER_NAME VARCHAR(200) NOT NULL, 84 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 85 | STR_PROP_1 VARCHAR(512) NULL, 86 | STR_PROP_2 VARCHAR(512) NULL, 87 | STR_PROP_3 VARCHAR(512) NULL, 88 | INT_PROP_1 INT NULL, 89 | INT_PROP_2 INT NULL, 90 | LONG_PROP_1 BIGINT NULL, 91 | LONG_PROP_2 BIGINT NULL, 92 | DEC_PROP_1 NUMERIC(13,4) NULL, 93 | DEC_PROP_2 NUMERIC(13,4) NULL, 94 | BOOL_PROP_1 VARCHAR(1) NULL, 95 | BOOL_PROP_2 VARCHAR(1) NULL, 96 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 97 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 98 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) 99 | ENGINE=InnoDB; 100 | 101 | CREATE TABLE QRTZ_BLOB_TRIGGERS ( 102 | SCHED_NAME VARCHAR(120) NOT NULL, 103 | TRIGGER_NAME VARCHAR(200) NOT NULL, 104 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 105 | BLOB_DATA BLOB NULL, 106 | PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), 107 | INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP), 108 | FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 109 | REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) 110 | ENGINE=InnoDB; 111 | 112 | CREATE TABLE QRTZ_CALENDARS ( 113 | SCHED_NAME VARCHAR(120) NOT NULL, 114 | CALENDAR_NAME VARCHAR(200) NOT NULL, 115 | CALENDAR BLOB NOT NULL, 116 | PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)) 117 | ENGINE=InnoDB; 118 | 119 | CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( 120 | SCHED_NAME VARCHAR(120) NOT NULL, 121 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 122 | PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)) 123 | ENGINE=InnoDB; 124 | 125 | CREATE TABLE QRTZ_FIRED_TRIGGERS ( 126 | SCHED_NAME VARCHAR(120) NOT NULL, 127 | ENTRY_ID VARCHAR(95) NOT NULL, 128 | TRIGGER_NAME VARCHAR(200) NOT NULL, 129 | TRIGGER_GROUP VARCHAR(200) NOT NULL, 130 | INSTANCE_NAME VARCHAR(200) NOT NULL, 131 | FIRED_TIME BIGINT(13) NOT NULL, 132 | SCHED_TIME BIGINT(13) NOT NULL, 133 | PRIORITY INTEGER NOT NULL, 134 | STATE VARCHAR(16) NOT NULL, 135 | JOB_NAME VARCHAR(200) NULL, 136 | JOB_GROUP VARCHAR(200) NULL, 137 | IS_NONCONCURRENT VARCHAR(1) NULL, 138 | REQUESTS_RECOVERY VARCHAR(1) NULL, 139 | PRIMARY KEY (SCHED_NAME,ENTRY_ID)) 140 | ENGINE=InnoDB; 141 | 142 | CREATE TABLE QRTZ_SCHEDULER_STATE ( 143 | SCHED_NAME VARCHAR(120) NOT NULL, 144 | INSTANCE_NAME VARCHAR(200) NOT NULL, 145 | LAST_CHECKIN_TIME BIGINT(13) NOT NULL, 146 | CHECKIN_INTERVAL BIGINT(13) NOT NULL, 147 | PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)) 148 | ENGINE=InnoDB; 149 | 150 | CREATE TABLE QRTZ_LOCKS ( 151 | SCHED_NAME VARCHAR(120) NOT NULL, 152 | LOCK_NAME VARCHAR(40) NOT NULL, 153 | PRIMARY KEY (SCHED_NAME,LOCK_NAME)) 154 | ENGINE=InnoDB; 155 | 156 | CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY); 157 | CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP); 158 | 159 | CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP); 160 | CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP); 161 | CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME); 162 | CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP); 163 | CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE); 164 | CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE); 165 | CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE); 166 | CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME); 167 | CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME); 168 | CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME); 169 | CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE); 170 | CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE); 171 | 172 | CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME); 173 | CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY); 174 | CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP); 175 | CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP); 176 | CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP); 177 | CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP); 178 | 179 | commit; 180 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.closer 5 | dynamic-table-mapper 6 | war 7 | 1.0-SNAPSHOT 8 | dynamic-table-mapper Maven Webapp 9 | http://maven.apache.org 10 | 11 | 12 | 13 | jdk17 14 | 15 | true 16 | 1.7 17 | 18 | 19 | 1.7 20 | 1.7 21 | 1.7 22 | 23 | 24 | 25 | 26 | 4.2.4.RELEASE 27 | 1.9.2.RELEASE 28 | 1.6.2.RELEASE 29 | 4.3.6.Final 30 | 2.4.5 31 | 32 | / 33 | 9088 34 | 9443 35 | 10080 36 | 37 | 38 | 39 | org.springframework 40 | spring-core 41 | ${spring.version} 42 | 43 | 44 | org.springframework 45 | spring-context 46 | ${spring.version} 47 | 48 | 49 | org.springframework 50 | spring-context-support 51 | ${spring.version} 52 | 53 | 54 | org.springframework 55 | spring-beans 56 | ${spring.version} 57 | 58 | 59 | org.springframework 60 | spring-aop 61 | ${spring.version} 62 | 63 | 64 | org.springframework 65 | spring-aspects 66 | ${spring.version} 67 | 68 | 69 | org.springframework 70 | spring-web 71 | ${spring.version} 72 | 73 | 74 | org.springframework 75 | spring-webmvc 76 | ${spring.version} 77 | 78 | 79 | org.springframework 80 | spring-expression 81 | ${spring.version} 82 | 83 | 84 | org.springframework 85 | spring-tx 86 | ${spring.version} 87 | 88 | 89 | org.springframework 90 | spring-orm 91 | ${spring.version} 92 | 93 | 94 | org.springframework 95 | spring-test 96 | ${spring.version} 97 | 98 | 99 | 100 | 101 | org.springframework.data 102 | spring-data-jpa 103 | ${spring-data-jpa.version} 104 | 105 | 106 | 107 | org.springframework.data 108 | spring-data-redis 109 | ${spring-data-redis.version} 110 | 111 | 112 | redis.clients 113 | jedis 114 | 2.8.0 115 | 116 | 117 | 118 | 119 | org.hibernate 120 | hibernate-core 121 | ${hibernate.version} 122 | 123 | 124 | org.hibernate 125 | hibernate-entitymanager 126 | ${hibernate.version} 127 | 128 | 129 | 130 | org.hibernate 131 | hibernate-validator 132 | 5.2.2.Final 133 | 134 | 135 | 136 | org.aspectj 137 | aspectjrt 138 | 1.8.0 139 | 140 | 141 | org.aspectj 142 | aspectjweaver 143 | 1.8.0 144 | 145 | 146 | 147 | org.hsqldb 148 | hsqldb 149 | 2.3.3 150 | 151 | 152 | 153 | javax.servlet 154 | javax.servlet-api 155 | 3.1.0 156 | 157 | 158 | 159 | com.google.guava 160 | guava 161 | 18.0 162 | 163 | 164 | 165 | com.fasterxml.jackson.core 166 | jackson-core 167 | ${jackson.version} 168 | 169 | 170 | 171 | com.fasterxml.jackson.core 172 | jackson-databind 173 | ${jackson.version} 174 | 175 | 176 | com.fasterxml.jackson.core 177 | jackson-annotations 178 | ${jackson.version} 179 | 180 | 181 | com.fasterxml.jackson.datatype 182 | jackson-datatype-hibernate4 183 | ${jackson.version} 184 | 185 | 186 | com.fasterxml 187 | classmate 188 | 1.3.1 189 | 190 | 191 | 192 | org.apache.commons 193 | commons-lang3 194 | 3.3.2 195 | 196 | 197 | commons-beanutils 198 | commons-beanutils 199 | 1.9.2 200 | 201 | 202 | 203 | org.quartz-scheduler 204 | quartz 205 | 2.2.2 206 | 207 | 208 | org.quartz-scheduler 209 | quartz-jobs 210 | 2.2.2 211 | 212 | 213 | 214 | org.slf4j 215 | slf4j-log4j12 216 | 1.7.13 217 | 218 | 219 | 220 | junit 221 | junit 222 | 4.11 223 | test 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | org.mortbay.jetty 235 | jetty-maven-plugin 236 | 8.1.16.v20140903 237 | 238 | 239 | ${jetty.context} 240 | 241 | 242 | 243 | ${jetty.http.port} 244 | 245 | 246 | jetty 247 | ${jetty.stopPort} 248 | 249 | 250 | 251 | 252 | dynamic-table-mapper 253 | 254 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于Spring框架快速开发 2 | 本框架并不是自己实现以下提及功能,而是基于Spring框架强大的集成能力,整合或配置, 3 | 形成一套完整的基于多数据源分库分表(关系型数据库)的支持多租户的解决方案。 4 | 5 | ## 使用到的相关开源技术及版本 6 | 7 | 1. Spring 4.2.4.RELEASE 8 | 2. Spring Data JPA 1.9.2.RELEASE 9 | 3. Spring Data Redis 1.6.2.RELEASE 10 | 4. Jackson 2.4.5 11 | 5. Hibernate 4.3.6.Final 12 | 6. Quartz 2.2.2 13 | 14 | 各开源项目之间可能会有版本不兼容,因此应该尽量小心,可以参考本配置。 15 | 16 | ## 分库支持 17 | 此处使用多数据源连接来实现分库,实现Spring提供的`AbstractRoutingDataSource`抽象类,仅需实现`determineCurrentLookupKey`方法即可。 18 | 19 | 实现多数据源支持 20 | 21 | ```java 22 | public class DynamicRoutingDataSource extends AbstractRoutingDataSource{ 23 | 24 | @Override 25 | protected Object determineCurrentLookupKey() { 26 | //返回当前上下文需要使用的数据源名称,与下面的ds1|ds2对应 27 | return TableProvider.getDataSoureName(); 28 | } 29 | } 30 | ``` 31 | 32 | 进行数据源配置 33 | 34 | ```java 35 | @Bean 36 | public DataSource dataSource() { 37 | //配置数据源 ds1 38 | JDBCDataSource ds1 = new JDBCDataSource(); 39 | //配置数据源 ds2 40 | JDBCDataSource ds2 = new JDBCDataSource(); 41 | 42 | Map targetDataSources = new HashMap<>(); 43 | targetDataSources.put("ds1", ds1); 44 | targetDataSources.put("ds2", ds2); 45 | 46 | DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(); 47 | //设置数据源映射 48 | dataSource.setTargetDataSources(targetDataSources); 49 | //设置默认数据源,当无法映射到数据源时会使用默认数据源 50 | dataSource.setDefaultTargetDataSource(ds1); 51 | dataSource.afterPropertiesSet(); 52 | return dataSource; 53 | } 54 | ``` 55 | 56 | ## 分表支持 57 | 58 | 由于`Spring Data JPA`无法对JPA规范中的`@Table`使用SpEL,所以无法直接使用`@Table`来动态实现表路由,此处借助Hibernate提供的`Interceptor`,我们可以看下官方对该接口的说明 59 | 60 | > Inspection occurs before property values are written and after they are read 61 | > from the database. 62 | 63 | 即使用该接口可以实现Hibernate与数据库进行交互时,对请求与返回进行处理。Hibernate 64 | 提供了这个拦截器的空实现`EmptyInterceptor`,此处我们只需继承并重写`onPrepareStatement` 65 | 方法即可。 66 | 表映射拦截器实现 67 | ```java 68 | public class TableMapperInterceptor extends EmptyInterceptor { 69 | 70 | @Override 71 | public String onPrepareStatement(String sql) { 72 | return sql.replace(TableProvider.PREFIX, TableProvider.getTablePrefix()); 73 | } 74 | } 75 | ``` 76 | 77 | 实体的配置 78 | ```java 79 | @Entity 80 | @Table(name = TableProvider.PREFIX + "_demo") 81 | public class Demo { 82 | 83 | @Id 84 | private Long id; 85 | private String name; 86 | 87 | //可以设置更多字段 88 | //此处省略getter/setter方法 89 | } 90 | ``` 91 | 92 | 注:Spring Data Mongo借助SpEL实现分表,可以阅读本人的博文 93 | [MongoDB分表与分片选择的一次实践](http://blog.csdn.net/zjl103/article/details/46927403) 94 | 95 | ## 多租户支持 96 | 97 | 多租户并不需要多实现什么,仅在进行数据库操作时,指定相应的租户识别号即可。 98 | 99 | ## 缓存支持 100 | 101 | 借助Spring对Cache的支持,我们可以“优雅”的使用缓存,并且是无需关心缓存实现,以下我们展示下缓存的配置与使用。 102 | 缓存配置,注册一个CacheManager实例 103 | ```java 104 | @Bean 105 | public CacheManager cacheManager() { 106 | SimpleCacheManager cacheManager = new SimpleCacheManager(); 107 | 108 | //配置Cache 109 | ConcurrentMapCacheFactoryBean cache1 = new ConcurrentMapCacheFactoryBean(); 110 | cache1.setName("tenants"); 111 | cache1.afterPropertiesSet(); 112 | Set caches = new HashSet<>(); 113 | caches.add(cache1.getObject()); 114 | //可以添加更多的Cache 115 | cacheManager.setCaches(caches); 116 | 117 | return cacheManager; 118 | } 119 | ``` 120 | 使用缓存,使用`@CacheConfig`及`@Cacheable` 121 | ``` 122 | @CacheConfig(cacheNames = "tenants") 123 | public class TenantRepositoryImpl extends SimpleJpaRepository { 124 | 125 | @Cacheable 126 | @Override 127 | public Tenant findOne(Long id) { 128 | return super.findOne(id); 129 | } 130 | } 131 | ``` 132 | Spring提供了多个缓存操作的注解 133 | 134 | > `@Cacheable` triggers cache population 135 | 136 | > `@CacheEvict` triggers cache eviction 137 | 138 | > `@CachePut` updates the cache without interfering with the method execution 139 | 140 | > `@Caching` regroups multiple cache operations to be applied on a method 141 | 142 | > `@CacheConfig` shares some common cache-related settings at class-level 143 | 144 | 通过上面的展示,我们的应用代码不依赖于具体的缓存实现,后续如果需要变更缓存实现, 145 | 我们无需更改我们的应用代码。而且对于后续扩展对新类型缓存的支持也将会很简便,该 146 | 缓存仅需实现Spring的`CacheManager`接口即可,同时这些注解支持使用SpEL来进行“自定义”。 147 | 目前Spring 支持的常用缓存类型有: 148 | 149 | 1. Guava 150 | 2. ConcurrentMap 151 | 3. EhCache 152 | 4. JCache 153 | 5. Redis,可以参考本项目的RedisConfig的配置 154 | 155 | 注: 156 | 157 | 1. 要特别注意分布式缓存的与Hibernate结合时,延迟加载属性的序列化问题。可以阅读 158 | 本人的博文[一个Memcache+Hibernate自处理二级缓存问题](http://blog.csdn.net/zjl103/article/details/45484633); 159 | 2. Spring官网推荐在具体类上使用`@Cache*`注解,而不是接口上; 160 | 3. `update`方法应该使用`@CacheEvict`而不是`@CachePut`(由于从RMDB操作完成到Cache操作已经不是一个原子操作,故在多线程情况下,你当前要Put的实体可能已经不是最新的了); 161 | 4. 要 *注意* cache与事务的关系,在Spring Cache中,缓存操作位于被注解方法的执行前后(可以通过),因此如遇到如下场景, 162 | 则会缓存到脏数据。缓存的相关实现可以查看代码,核心代码`CacheInterceptor`。部分`CacheManager`继承自`AbstractTransactionSupportingCacheManager`, 163 | 则可通过`setTransactionAware(true)`来使用得缓存在事务提交后执行。 164 | 165 | ```java 166 | //annotate with @CacheEvict 167 | repository.save(t); 168 | //annotate with @Cacheable 169 | repository.findOne(t.getId()); 170 | if (true) { 171 | throw new RuntimeException(""); 172 | } 173 | ``` 174 | 175 | ## 整型分布式唯一主键生成 176 | * 为何不使用UUID? 177 | * 存储空间翻倍,UUID长度是128bit,BigInt是64bit 178 | * 索引性能比BigInt差,同时无序的UUID对Mysql的Innodb聚簇索引性能影响更大,会使索引树频繁的重建。 179 | * 为何不使用自增 180 | * 自增只能保证表唯一,无法保证各分表与分库唯一 181 | * 实现参考:[snowflake的JAVA版本(分布式唯一ID生成器)](http://www.oschina.net/code/snippet_147955_25122) 182 | * 原文中提到的实现为一个独立的主键生成服务,为了简化部署,这里将实现集成至服务内,而workId则通过Redis来生成,具体实现细节可以查看`DistributedIdentifierGenerator`类注释 183 | 184 | ## 任务调度及持久化 185 | Spring本身已可以很好的支持任务调度,通过`@EnableScheduling`注解打开任务调度,使用`@Scheduled`对方法进行注解, 186 | 使其成为一个任务。但该方式在集群方式却是有很大的局限性。因此引入Quartz,同时可确保持久化任务,我们使用了其持久化功能。 187 | 188 | 1. 使用`/init-sql`目录下,选择合适类型的数据库脚本,初始化Quartz表; 189 | 2. 新增配置`QuartzConfig`,配置`SchedulerFactoryBean`及相应的`JobDetail`,可参考`HappyBirthdayJob`; 190 | 3. 创建`Trigger`,可参考`HappyBirthdayJob.trigger`; 191 | 192 | ## 框架为我们做了什么 193 | 194 | ### 简化Dao层的开发 195 | 借助Spring Data,通过方法命名式来定义数据库查询(0 sql)。 196 | ```java 197 | @Repository 198 | public interface UserRepository extends JpaRepository { 199 | User findByName(String name); 200 | } 201 | ``` 202 | 我们通过定义接口`UserRepository`的`findByName`方法,就实现了通过用户名获取用户的数据库查询。同时在`JpaRepository`中提供了基础的、基于泛型的CRUD数据库操作。 203 | 204 | 注:Spring在服务启动时会,会检测方法名是否合法,如对于`findByName`方法,如果实体没有`name`属性,将会报错,因此基于此种方式构建的Dao层将会更“可信赖”,可以减少我们的测试量。 205 | 206 | ### 同一实体返回不同投影给客户端 207 | 我们经常会遇到【列表请求】与【详情请求】需要包含不同的字段,以减小网络的传输量,借助Spring Mvc对`@JsonView`的支持,我们无需再定义额外`Vo`来实现。 208 | 209 | 控制器定义 210 | ```java 211 | @RestController 212 | @RequestMapping("/companies") 213 | public class EmployeeController { 214 | 215 | @Autowired 216 | private EmployeeService service; 217 | 218 | @JsonView(DetailView.class) 219 | @RequestMapping(value = "/employees/{employeeId}",method = RequestMethod.GET) 220 | public Employee get(@PathVariable("employeeId") long employeeId) { 221 | return service.findOne(employeeId); 222 | } 223 | 224 | @JsonView(ListView.class) 225 | @RequestMapping(value = "{companyId}/employees",method = RequestMethod.GET) 226 | public List list(@PathVariable long companyId) { 227 | return service.findAll(); 228 | } 229 | } 230 | ``` 231 | 实体定义 232 | ```java 233 | @Entity 234 | @Table(name = TableProvider.PREFIX_ + "employee") 235 | public class Employee extends BaseTenantDomain { 236 | 237 | public interface DetailView{} 238 | 239 | public interface ListView{} 240 | 241 | private String name; 242 | 243 | private String enName; 244 | 245 | @JsonView(Detail.class) 246 | private Department department; 247 | 248 | //此处省略getter/setter方法 249 | } 250 | 251 | ``` 252 | 我们在控制器方法`get`指定`@JsonView(DetailView.class)`表示返回的Json将仅包含`Employee`中未使用`@JsonView`注解及使用`@JsonView`注解且包含DetailView.class的字段。 253 | 254 | 注:`@JsonView`在使用的时候要注意,多个有互相关联的实体间,不要使用公共定义的`ViewClass`,应该在类中自己定义,以免容易互相影响。 255 | 256 | ### 审计 257 | Spring Data提供了`@CreatedDate`(创建时间)、`@CreatedBy`(创建人)、`@LastModifiedDate`(最后更新时间)、`@LastModifiedBy`(最后更新人)四个注解来注解实体字段,被注解的字段在实体保存时将会被自动赋值。 258 | 259 | 使用`@EnableJpaAuditing`启用审计 260 | ```java 261 | @Configuration 262 | @EnableJpaAuditing 263 | public class RDMSConfig { 264 | } 265 | ``` 266 | 实体定义 267 | ```java 268 | @Entity 269 | @EntityListeners(AuditingEntityListener.class) 270 | public class Demo { 271 | 272 | @Id 273 | @GeneratedValue 274 | private Long id; 275 | 276 | @LastModifiedDate 277 | private long updateTime; 278 | 279 | @CreatedDate 280 | @Column(updatable = false) 281 | private long createTime; 282 | 283 | //省略getter/setter方法 284 | } 285 | ``` 286 | 注意: 287 | * `@CreateDate`需要同时使用`@Column(updatable = false)`注解,否则实体更新时,可能会被覆盖。 288 | * 实体需要使用`@EntityListeners(AuditingEntityListener.class)`注解 289 | * `@CreatedBy`与`@LastModifiedBy`会稍微复杂点,请阅读[Spring Data JPA Reference](http://docs.spring.io/spring-data/jpa/docs/1.8.2.RELEASE/reference/html/)。 290 | 291 | ### 实现一个继承体系多类型ID及生成器 292 | 有时在继承体系中,我们需要使用不同类型的ID,如Long或String,或需要使用到不同类型的ID生成器,如identity或uuid,为了避免因此我们要重起一个继承体系,我们可以使用此处提到的方法。 293 | 定义基类,id使用泛型 294 | ```java 295 | @MappedSuperclass 296 | public class BaseIdDomain { 297 | 298 | @Id 299 | @GeneratedValue(generator="id") 300 | private ID id; 301 | 302 | //省略getter/setter方法 303 | } 304 | ``` 305 | 定义子类,此处指定了id为`String`类型,使用`@GenericGenerator`来指定具体的策略。 306 | ```java 307 | @Entity 308 | @GenericGenerator(name = "id", strategy = "uuid2") 309 | public class Demo extends BaseIdDomain { 310 | 311 | //省略其它的字段定义 312 | } 313 | ``` 314 | 注意 315 | * 这里的`name`需要与基类`@GeneratedValue`中的`generator`属性相同。 316 | * 这里的`strategy`可以是Hibernate已建,也可以使用自定义的,自定义的使用全限定路径,实现`IdentifierGenerator`接口。以下为常用的已建策略,更多已建策略可以查看`DefaultIdentifierGeneratorFactory`类。 317 | 318 | sequence: 调用底层数据库的序列来生成主键,要设定序列名,不然hibernate无法找到。 319 | identity: 使用SQL Server和MySQL的自增字段,Oracle 不支持自增字段 320 | uuid和uuid.hex: 两者使用同一个生成器,生成不带分隔符的uuid,32位 321 | uuid2: 生成带分隔符的uuid(8-4-4-4-12),36位 322 | assigned: 根据用户设置值来做id,如果用户不设置,会抛出异常 323 | 324 | 325 | ### 提供增量式修改方法 326 | 有时我们仅需要修改部分字段,而传统`update`方法为覆盖式的,当你的某个字段没传时,也会被覆盖为空,因此此处特地增加了`BaseService.update(Long id, Map map)`方法,允许增量修改字段,而不需要修改的可以不传。 327 | 328 | 注:还有另外一种相似的修改场景,操作A仅能修改字段集合a,操作B仅能修改字段集合b,目前暂未支持该功能,但应该可以结合类似`@JsonView`的方式来实现。 329 | 330 | ## 一些思考 331 | 本人的一些思考分享,不一定对,欢迎探讨 332 | 333 | ### Valid的时机: 334 | 1. 数据库保存时,即Servicer与Dao的中间层(或Dao层),该步骤应该是必须的。 335 | 2. 在进行业务逻辑处理前,如Controller或Service的第一步,此步骤非必须,但如果使用Valid,则可以省去很多进行业务逻辑时的数据有效性判断(但是当数据需要由业务逻辑补充时,则1与2无法使用同一套验证,此时可以考虑使用group),但如果无业务逻辑处理时,则可以完全委托给第1点所述时验证。 336 | 337 | ### 缓存操作应该放在哪 338 | 1. 对实体的缓存操作,Service层不应该关心数据是从数据库还是从缓存中读取到,即缓存的操作应该是在Dao层且对用户透明的,如Hibernate对二级缓存的使用。 339 | 2. 非实体的缓存操作,仅当你需要缓存的与数据库实体不同时,才需要通过Service层手动操作,如xx标志。 340 | 341 | 342 | ### Service层是否需要接口 343 | 如果代码(非框架代码)编写过程需要经常改动到继承体系中的代码,意味着无法形成稳定的契约,则该继承体系根本不需要存在。我们常说的面向接口编程,是为了不依赖于实现,而对于业务系统来说,则没有多种业务实现。 344 | 345 | 在我们最常使用接口的MVC三层架构中,原来的接口可能是为了使用Proxy来提供动态代理,因此必须要定义接口,而现在使用cglib,则没必要了。 346 | 347 | 简单来说,继承体系仅对框架有意义,而对于业务代码,则会成为开发与维护的累赘。 348 | 349 | ### 查询是否需要使用事务 350 | 援引网上观点,已找不出处 351 | > 1. 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性; 352 | > 2. 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。 353 | 354 | 因此可以使用`@Transactional`在基类设置使得全部方法都默认使用事务,同时再使用`@Transactional(propagation = Propagation.SUPPORTS)`来对单个方法排除掉事务,主要目的是为了避免遗漏注解必须使用事务的方法造成不良后果。 355 | 356 | 注:在配置`TransactionManager`,需要设置`AbstractPlatformTransactionManager.setTransactionSynchronization(AbstractPlatformTransactionManager.SYNCHRONIZATION_ON_ACTUAL_TRANSACTION)`,不要问我为什么,阅读下`Propagation.SUPPORTS`的注释及相关代码注释。 357 | 358 | ## 问题解决及注意事项 359 | 360 | 此处记录本人在整合过程中遇到的一些问题及后续引用该框架的开发者需要注意的事项。 361 | 362 | ### 动态`DataSource`的问题,此处的实现为`DynamicRoutingDataSource` 363 | `Service`方法(包括`Service`方法再调用其它方法)将会共用同一个数据库连接,即在`Service`方法,无法进行数据源的切换,进入`Service`方法时,会从`DataSource`中请求数据库连接,这也使事务得到发挥作用。如果要切换`DataSource`,则可以使用切换线程来实现 364 | 365 | ### `@Async`在debug状态下,会使整个请求挂起,此时不要误认为异步无效。 366 | 367 | ### Spring Cache使用限制 368 | `@Cacheable`可以注解在`Repository`上,但如果是`JpaRepository`自带的默认方法,则无效,因为Spring直接注入的是SimpleJpaRepository实例。 369 | 370 | ```java 371 | @Repository 372 | @CacheConfig(cacheNames = "companies") 373 | public interface CompanyRepository extends JpaRepository{ 374 | 375 | @Override 376 | @Cacheable 377 | Company findOne(Long id); 378 | } 379 | 380 | @Service 381 | public class CompanyService extends BaseService{ 382 | 383 | @Autowired 384 | private CompanyRepository repository; 385 | 386 | //如果这里不写repository.findOne(id),而是使用的是BaseService的findOne,则会无效 387 | @Override 388 | public Company findOne(Long id) { 389 | return repository.findOne(id); 390 | } 391 | } 392 | 393 | ``` 394 | 可以使用以上的方式来实现,但当你需要有一个基础`BaseService`时,就无法使用此方式,此时只能重新实现`Repository` 395 | 396 | ```java 397 | public interface CompanyOtherRepository { 398 | Company findOne(Long id); 399 | } 400 | 401 | @Repository 402 | public interface CompanyRepository extends JpaRepository,CompanyOtherRepository { 403 | } 404 | 405 | @Repository 406 | @CacheConfig(cacheNames = "companies") 407 | public class CompanyRepositoryImpl extends SimpleJpaRepository implements CompanyOtherRepository { 408 | 409 | @Autowired 410 | public CompanyRepositoryImpl(EntityManager entityManager) { 411 | super(Company.class, entityManager); 412 | } 413 | 414 | @Cacheable 415 | @Override 416 | public Company findOne(Long id) { 417 | return super.findOne(id); 418 | } 419 | } 420 | 421 | ``` 422 | 注:此处`CompanyOtherRepository`不是必需的 423 | 424 | ### 解决关联实体JSON序列化死循环问题 425 | 1. 使用`@JsonIgnore`来使用得一方可以不被序列化,常用于`1 vs n`中 `1`端的`n`属性上 426 | 2. 使用`@JsonManagedReference`与`@JsonBackReference`对,被`@JsonBackReference`注解的属性将不会被序列化出来,目前看不出跟@JsonIgnore有啥区别 427 | 3. 使用`@JsonIgnoreProperties`,如在注解在`User`的`roles`属性上 428 | 429 | ```java 430 | public class User{ 431 | @JsonIgnoreProperties("users") 432 | private List roles; 433 | 434 | //.... another code 435 | } 436 | ``` 437 | 438 | 综上的方式,1跟2没有区别,可以使用1来得简单,1与3的效果不同,所以在使用上可以很容易区分。 439 | 440 | 441 | ### 解决Json无法反序列化`List` 442 | 反序列化时可以指定类型如 443 | ```java 444 | ObjectMapper mapper = new ObjectMapper(); 445 | mapper.readValue(json,Demo.class); 446 | ``` 447 | 当json为Demo的一个实例时,可以反序列化成功,当为Demo的数组时,则会失败,此时可以使用以下方法来处理。 448 | ```java 449 | ObjectMapper mapper = new ObjectMapper(); 450 | mapper.readValue(json, 451 | mapper.getTypeFactory().constructCollectionType(List.class, Demo.class)); 452 | ``` 453 | 454 | 455 | ## 参考文献 456 | [Spring Framework Reference](http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/) 457 | 458 | [Spring Data JPA Reference](http://docs.spring.io/spring-data/jpa/docs/1.8.2.RELEASE/reference/html/) 459 | 460 | [Spring Data Redis Reference](http://docs.spring.io/spring-data/redis/docs/1.6.2.RELEASE/reference/html/) 461 | 462 | [Quartz Scheduler Example Programs and Sample Code](http://www.quartz-scheduler.org/generated/2.2.2/pdf/Quartz_Scheduler_Example_Programs_and_Sample_Code.pdf) 463 | 464 | [Quartz Scheduler Configuration Guide](http://www.quartz-scheduler.org/generated/2.2.2/pdf/Quartz_Scheduler_Configuration_Guide.pdf) 465 | --------------------------------------------------------------------------------