一、添加依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--引入阿里的TransmittableThreadLocal -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.12.1</version>
</dependency>
二、编写yaml 配置 文件:application-dev.yaml
#mybatis-plus配置项
mybatis-plus:
mapperLocations:
typeAliasesPackage:
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
global-config:
db-config:
#配置全局逻辑删除,指定实体类的逻辑删除字段
logic-delete-field: delFlag
logic-delete-value: 1
logic-not-delete-value: 0
#配置主键id生成策略采用雪花算法
id-type: assign-id
#自定义多租户配置
exam:
tenant:
#启用多租户
enable: true
#指定哪些表不走多租户
ignoreTables:
# - exam_subject
# - exam_subject_category
#数据库租户id字段
column: tenant_id
三、主键id使用雪花算法生成策略
在配置文件上面配置了之后,在对应的实体类的id属性上添加 如下注解
@TableId(value="id",type=IdType.ASSIGN_ID)
四、配置字段自动填充
编写一个自定义类实现 MetaObjectHandler 接口
public class MateMetaObjectHandler implements MetaObjectHandler{
//此处的createTime 和 modifiedTime 也是对应实体类的属性名称,而非数据库字段名
@Override
public void insertFill(MetaObject metaObject){
this.strictInsertFill(metaObject,"createTime",LocalDateTime.class,LocalDateTime.now());
this.strictInsertFill(metaObject,"modifiedTime",LocalDateTime.class,LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject){
this.strictUpdateFill(metaObject,"modifiedTime",LocalDateTime.class,LocalDateTime.now());
}
}
然后需要在需要进行自动填充的实体类上添加如下注解
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
@TableFidld(value="create_time",fill = FieldFill.INSERT)
private LocalDateTime createTime;
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
@TableFidld(value="modified_time",fill = FieldFill.INSERT_UPDATE)
private LocalDateTime modifiedTime;
五、配置分页,多租户,防止全表更新或删除插件
package com.dzx.ccb.exam.exammanage.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
//import org.springframework.cloud.context.config.annotation.RefreshScope;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author DuanZhaoXu
* @ClassName:
* @Description:
* @date 2021年06月03日 17:36:15
*/
@Getter
@Setter
//@RefreshScope
@ConfigurationProperties(prefix = "exam.tenant")
public class TenantProperties {
/**
* 是否开启租户模式
*/
private Boolean enable = false;
/**
* 需要排除的多租户的表
*/
private List<String> ignoreTables = new ArrayList();
/**
* 多租户字段名称
*/
private String column = "tenant_id";
// /**
// * 排除不进行租户隔离的sql
// * 样例全路径:vip.mate.system.mapper.UserMapper.findList
// */
// private List<String> ignoreSqls = new ArrayList<>();
}
package com.dzx.ccb.exam.exammanage.config;
import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.experimental.UtilityClass;
/**
* 多租户Holder
*
* @author pangu
* @since 2020-9-8
*/
@UtilityClass
public class TenantContextHolder {
/**
* 支持父子线程之间的数据传递
*/
private final ThreadLocal<String> THREAD_LOCAL_TENANT = new TransmittableThreadLocal<>();
/**
* TTL 设置租户ID<br/>
* <b>谨慎使用此方法,避免嵌套调用。尽量使用 {@code TenantBroker} </b>
*
* @param tenantId 租户ID
*/
public void setTenantId(String tenantId) {
THREAD_LOCAL_TENANT.set(tenantId);
}
/**
* 获取TTL中的租户ID
*
* @return String
*/
public String getTenantId() {
return THREAD_LOCAL_TENANT.get();
}
/**
* 清除tenantId
*/
public void clear() {
THREAD_LOCAL_TENANT.remove();
}
}
package com.dzx.ccb.exam.exammanage.config;
/**
* 多租户常量
* @author pangu
* @date 2020-9-7
*/
public interface TenantConstant {
/**
* header 中租户ID
*/
String MATE_TENANT_ID = "mate-tenant";
/**
* 租户id参数
*/
String MATE_TENANT_ID_PARAM = "tenantId";
/**
* 租户ID
*/
String TENANT_ID_DEFAULT = "1";
}
package com.dzx.ccb.exam.exammanage.config;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import lombok.AllArgsConstructor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.NullValue;
import net.sf.jsqlparser.expression.StringValue;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 多租户配置中心
*
* @author pangu
* @Date 2020-9-7
*/
@Configuration
@AllArgsConstructor
@AutoConfigureBefore(MybatisPlusConfiug.class)
@EnableConfigurationProperties(TenantProperties.class)
public class TenantConfiguration {
private final TenantProperties tenantProperties;
/**
* 新多租户插件配置,一缓和二缓遵循mybatis的规则,
* 需要设置 MybatisConfiguration#useDeprecatedExecutor = false
* 避免缓存万一出现问题
*
* @return TenantLineInnerInterceptor
*/
@Bean
public TenantLineInnerInterceptor tenantLineInnerInterceptor() {
return new TenantLineInnerInterceptor(new TenantLineHandler() {
/**
* 获取租户ID
* @return Expression
*/
@Override
public Expression getTenantId() {
String tenant = TenantContextHolder.getTenantId();
if (tenant != null) {
return new StringValue(TenantContextHolder.getTenantId());
}
return new NullValue();
}
/**
* 获取多租户的字段名
* @return String
*/
@Override
public String getTenantIdColumn() {
return tenantProperties.getColumn();
}
/**
* 过滤不需要根据租户隔离的表
* 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
* @param tableName 表名
*/
@Override
public boolean ignoreTable(String tableName) {
return tenantProperties.getIgnoreTables().stream().anyMatch(
(t) -> t.equalsIgnoreCase(tableName)
);
}
});
}
}
package com.dzx.ccb.exam.exammanage.config;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 租户上下文过滤器
*
* @author pangu
* @date 2020-9-7
*/
@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@EnableConfigurationProperties(TenantProperties.class)
@AllArgsConstructor
public class TenantContextHolderFilter extends GenericFilterBean {
private final TenantProperties tenantProperties;
@Override
@SneakyThrows
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (tenantProperties.getEnable()) {
try {
//优先取请求参数中的tenantId值
String tenantId = request.getHeader(TenantConstant.MATE_TENANT_ID);
if (StringUtils.hasLength(tenantId)) {
TenantContextHolder.setTenantId(tenantId);
} else {
TenantContextHolder.setTenantId(TenantConstant.TENANT_ID_DEFAULT);
}
log.info("获取到的租户ID为:{}", TenantContextHolder.getTenantId());
filterChain.doFilter(request, response);
} finally {
TenantContextHolder.clear();
}
} else {
filterChain.doFilter(request, response);
}
}
}
最后 编写 MybatisPlusConfig类 把上面的配置类进行整合
package com.dzx.ccb.exam.exammanage.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import lombok.AllArgsConstructor;
import net.sf.jsqlparser.expression.LongValue;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.Arrays;
import java.util.List;
/**
* @author 80004819
* @ClassName:
* @Description:
* @date 2020年08月29日 09:43:44
*/
@AllArgsConstructor
@EnableTransactionManagement
@Configuration
@MapperScan(value = "com.ccb.cloud.cu.exam.admin.mapper")
public class MybatisPlusConfiug {
private final TenantProperties tenantProperties;
private final TenantLineInnerInterceptor tenantLineInnerInterceptor;
/**
* 单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法)
*/
private static final Long MAX_LIMIT = 1000L;
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,
* 需要设置 MybatisConfiguration#useDeprecatedExecutor = false
* 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
boolean enableTenant = tenantProperties.getEnable();
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//如果启用了多租户,则把自定义的租户插件添加进来
if (enableTenant) {
interceptor.addInnerInterceptor(tenantLineInnerInterceptor);
}
//分页插件: PaginationInnerInterceptor
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInnerInterceptor.setMaxLimit(MAX_LIMIT);
//防止全表更新与删除插件: BlockAttackInnerInterceptor
BlockAttackInnerInterceptor blockAttackInnerInterceptor = new BlockAttackInnerInterceptor();
interceptor.addInnerInterceptor(paginationInnerInterceptor);
interceptor.addInnerInterceptor(blockAttackInnerInterceptor);
return interceptor;
}
/**
* 自动填充数据
*/
@Bean
// @ConditionalOnMissingBean(MateMetaObjectHandler.class)
public MateMetaObjectHandler mateMetaObjectHandler() {
return new MateMetaObjectHandler();
}
/**
* mybatis-plus 乐观锁拦截器
*/
@Bean
public OptimisticLockerInnerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInnerInterceptor();
}
}
如果需要对某一个指定的mapper 或者 mapper 层的 sql 进行 不走多租户模式,则可以在mapper 或者 mapper.method 上 添加
true 表示不走插件(在配置了插件的情况下,不填则默认表示为false)
@InterceptorIgnore(tenantLine="true")