# diboot 常见问题FAQ
# diboot支持Spring Boot版本情况
- diboot 2.6.x 支持 Spring boot 2.7.x
- diboot 2.5.x 支持 Spring boot 2.6.x
- diboot 2.4.x 支持 Spring boot 2.6.x
- diboot 2.3.x 支持 Spring boot 2.5.x
# IAM的后端代码在哪里?
IAM的controller等后端基础代码由devtools自动生成:
- 配置好diboot组件依赖和devtools依赖
- 启动项目,进入devtools的组件初始化页面,选择core及IAM等组件,执行初始化
- devtools将生成IAM基础的代码到你配置的路径下
# 自定义jackson配置
diboot-core-starter中包含默认的HttpMessageConverters配置,启用jackson并做了初始化配置。 其中关键配置参数为:
@Bean
@ConditionalOnMissingBean
public MappingJackson2HttpMessageConverter jacksonMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = converter.getObjectMapper();
...
return converter;
}
2
3
4
5
6
7
8
如果该配置无法满足您的开发场景,可以在Configuration文件中重新定义HttpMessageConverters:
@Bean
public HttpMessageConverters jacksonHttpMessageConverters() {
...
}
2
3
4
# 引入diboot-*-starter后,SQL分页查询出现重复的LIMIT
LIMIT ? LIMIT ?
重复定义了分页导致的,diboot-core-starter默认预置了mybatis-plus的分页配置(使用mybatis-plus 3.4.x的MybatisPlusInterceptor最新配置方式)。 如果您依赖的是core-starter,则无需再次配置mybatis-plus的分页,将您自定义的mybatis-plus分页配置删掉即可。 如果需要添加其他Interceptor,则需要重新定义MybatisPlusInterceptor。 示例如下:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
2
3
4
5
6
# 老项目中使用diboot的绑定能力
mybatis-plus老项目想要使用diboot的绑定能力或在无数据库连接配置文件的module下,使用diboot-core?
对于没有历史包袱的新项目,我们建议您使用diboot全套体系。 core内核组件有以下两个包:
- diboot-core: 内核代码
- diboot-core-starter: 依赖diboot-core,增加了自动配置及初始化字典表等功能(需要依赖数据库信息)。
对于mybatis-plus老项目中想要使用diboot的绑定能力 或 在无数据库连接配置文件的module下,使用内核组件,可以只依赖diboot-core,而不是diboot-core-starter。 步骤如下:
- 添加core依赖(非core-starter)
<dependency>
<groupId>com.diboot</groupId>
<artifactId>diboot-core</artifactId>
<version>{latestVersion}</version>
</dependency>
2
3
4
5
- 如果只依赖core,你还需要将com.diboot.core加入包扫描并实现HttpMessageConverters和Mybatis-plus的分页配置:
@ComponentScan(basePackages={"com.diboot.core"})
@MapperScan(basePackages = {"com.diboot.core.mapper"})
2
- 如果只依赖core,且需要使用@BindDict字典绑定,需实现DictionaryServiceExtProvider接口。 (使用diboot-core-starter可以自动创建dictionary表,或者可以下载SQL (opens new window)手动建表。
# 启动报错:找不到mapper中的自定义接口
diboot-devtools默认不指定mapper.xml路径时,mapper.xml文件会生成到mapper同路径下便于维护。 此时需要修改pom配置,让编译包含xml、dtd类型文件。
- Maven配置:
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.dtd</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- Gradle配置:
sourceSets {
main {
resources {
srcDirs "src/main/java"
include '**/*.xml'
include '**/*.dtd'
include '**/*.class'
}
resources {
srcDirs "src/main/resources"
include '**'
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 如何构建树形结构?
树形结构对象约定:要有 parentId属性(根节点为0) 和 List children 属性,便于自动构建。
- 先把需要构建树形结构的节点全部查出来,如:
List<Menu> menus = menuService.getEntityList(wrapper);
- 调用BeanUtils.buildTree构建树形结构
// 如果children属性在VO中,可以调用BeanUtils.convertList转换后再构建
menus = BeanUtils.buildTree(menus);
2
返回第一级子节点集合。
# 查询Date类型日期范围,如何自动绑定?
使用Comparison.GE,Comparison.LT进行绑定,避免数据库转型。
/**
* 创建时间-起始
*/
@BindQuery(comparison = Comparison.GE, field = "createTime")
private Date createTimeBegin;
/**
* 创建时间-截止(截止时间<=当天23:59:59是不精确的,应该是<第二天)
*/
@BindQuery(comparison = Comparison.LT, field = "createTime")
private Date createTimeEnd;
public TodoRemider setCreateTimeEnd(Date createTimeEnd) {
this.createTimeEnd = D.nextDay(createTimeEnd);
return this;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查询Date类型日期时间字段 = 某天,如何自动绑定?
建议逻辑: datetime_field >= beginDate AND datetime_field < (beginDate+1) 。 无函数处理,不涉及数据库类型转换。示例:
/**
* 创建时间-起始
*/
@BindQuery(comparison = Comparison.GE, field = "createTime")
private Date createTime;
/**
* 创建时间-截止
*/
@BindQuery(comparison = Comparison.LT, field = "createTime")
private Date createTimeEnd;
public Date getCreateTimeEnd() {
return D.nextDay(createTime);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 如何自动填充创建人、创建时间、更新时间等字段
- 创建时间、更新时间首选采用数据库填充方式实现
- 如需代码自动填充的字段,可通过Mybatis-plus的MetaObjectHandler自动填充, 具体请参考mybatis-plus文档 (opens new window)。 示例: 注解标记填充字段:
class MyEntity {
@TableField(fill = FieldFill.INSERT)
private Long createBy;
...
}
2
3
4
5
实现填充Handler:
@Component
public class AutoFillMetaObjectHandler implements MetaObjectHandler{
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, Cons.FieldName.createBy.name(), IamSecurityUtils::getCurrentUserId, Long.class);
// this.strictInsertFill(metaObject, Cons.FieldName.createTime.name(), LocalDateTime::now, LocalDateTime.class);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, Cons.FieldName.updateBy.name(), IamSecurityUtils::getCurrentUserId, Long.class);
// this.strictUpdateFill(metaObject, Cons.FieldName.updateTime.name(), LocalDateTime::now, LocalDateTime.class);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 如何配置swagger接口文档(以SpringDoc实现为例)
因springfox更新缓慢,无法执行spring boot 2.6版本,diboot推荐使用SpringDoc替代springfox。
步骤1. pom中引入SpringDoc依赖
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.2</version>
</dependency>
2
3
4
5
步骤2. 添加配置类
// 示例配置类
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI initOpenAPI() {
return new OpenAPI().info(
new Info().title("My Project API").description("OpenAPI").version("v1.0")
);
}
}
2
3
4
5
6
7
8
9
10
步骤1&2为swagger的正常配置,如果引入了diboot IAM组件,需要添加以下配置使swagger相关url可以匿名访问。
步骤3. 设置swagger相关的匿名url配置,使swagger不被拦截, 如下:
#swagger 3.x版本参考配置
diboot.iam.anon-urls=/swagger**/**,/v3/**
2
另外,如果启用了diboot devtools,可以配置devtools生成代码启用swagger注解。
diboot.devtools.enable-swagger=true
附: swagger访问入口地址:
- swagger接口文档入口地址: /{contextPath}/swagger-ui.html
- springdoc 官方文档: https://springdoc.org (opens new window)
# 如何启用多租户
- diboot单体版本: diboot的系统表均预留了多租户字段 tenant_id ,启用 Mybatis-plus的租户拦截器 (opens new window) 即可启用多租户能力。 需自行实现多租户管理功能及租户权限配置。 启用方式:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 建议严格遵循以下顺序添加拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 租户拦截器(可选)
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new LongValue(0);
}
// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
@Override
public boolean ignoreTable(String tableName) {
return "iam_account".equalsIgnoreCase(tableName) || S.startsWithIgnoreCase(tableName, "diboot_");
}
}));
// 数据权限拦截器(可选)
interceptor.addInnerInterceptor(new DataAccessControlInteceptor());
// 分页拦截器,需保证 add PaginationInnerInterceptor 在最后
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- diboot-cloud微服务版本: 已预置多租户权限体系,租户管理能力。了解diboot-cloud微服务版多租户能力
# 页面后台报如下错误:
Unable to correctly extract the Initialization Vector or ciphertext 异常信息:
o.a.shiro.mgt.DefaultSecurityManager: Delegate RememberMeManager instance of type [org.apache.shiro.web.mgt.CookieRememberMeManager] threw an exception during getRememberedPrincipals().
org.apache.shiro.crypto.CryptoException: Unable to correctly extract the Initialization Vector or ciphertext.
at org.apache.shiro.crypto.JcaCipherService.decrypt(JcaCipherService.java:378) ~[shiro-core-1.8.0.jar:1.8.0]
...
...
Caused by: java.lang.ArrayIndexOutOfBoundsException: null
at java.lang.System.arraycopy(Native Method) ~[na:1.8.0_221]
at org.apache.shiro.crypto.JcaCipherService.decrypt(JcaCipherService.java:370) ~[shiro-core-1.8.0.jar:1.8.0]
... 44 common frames omitted
2
3
4
5
6
7
8
9
问题原因: shiro的rememberMe策略与cookie相关问题引起。
解决方案: 直接清空浏览器的cookie的缓存,建议清空所有的cookie,问题可以得到解决。极有可能是当前浏览器在其他的网站中有过rememberMe的记录导致后台的shiro会用cookie去反序列化导致。
# Java 实现跨域方式
# 全局跨域
// 定义跨域filter
public class CustomCorsFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
// 告诉浏览器允许所有的域访问
// 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
// resp.addHeader("Access-Control-Allow-Origin", "*");
// 解决办法通过获取Origin请求头来动态设置
String origin = request.getHeader("Origin");
if (StringUtils.hasText(origin)) {
resp.addHeader("Access-Control-Allow-Origin", "*");
}
// 允许带有cookie访问
resp.addHeader("Access-Control-Allow-Credentials", "true");
// 告诉浏览器允许跨域访问的方法
resp.addHeader("Access-Control-Allow-Methods", "*");
// 设置支持所有的自定义请求头
String headers = request.getHeader("Access-Control-Request-Headers");
if (StringUtils.hasText(headers)) {
resp.addHeader("Access-Control-Allow-Headers", headers);
}
// 告诉浏览器缓存OPTIONS预检请求1小时,避免非简单请求每次发送预检请求,提升性能
resp.addHeader("Access-Control-Max-Age", "3600");
chain.doFilter(request, resp);
}
}
// 初始化filter
@Bean
public FilterRegistrationBean customCorsFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean(new CustomCorsFilter());
//过滤所有路径
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(1);
return registrationBean;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 局部跨域
- 在控制器(类上)上使用注解
@CrossOrigin
,表示该类的所有方法允许跨域。
@RestController
@CrossOrigin("*")
public class VerificationController {
}
2
3
4
5
- 在方法上使用注解
@CrossOrigin
@PostMapping("/check/phone")
@CrossOrigin("*")
public boolean checkPhoneNumber(@RequestBody @ApiParam VerificationPojo verification) throws BusinessException {
return false;
}
2
3
4
5