文章524
标签277
分类67

EZShare项目总结-3

本篇在第二篇的基础之上完成项目配置和Spring-Boot框架之间的整合, 然后完成了一些工具类的创建和单元测试, 最后通过完成Dict类的相关业务逻辑, 完成配置集成测试

具体代码见: https://github.com/JasonkayZK/EZShare

欢迎PR❤


EZShare项目总结-3

本篇主要分为三个部分:

  • 配置整合
  • 通用工具类
  • Dict业务代码测试

配置整合

① Mybatis-Plus配置

application.yml

1
2
3
4
5
6
7
8
mybatis-plus:
type-aliases-package: top.jasonkayzk.ezshare.system.entity,top.jasonkayzk.ezshare.job.entity,top.jasonkayzk.ezshare.file.entity
mapper-locations: classpath:mapper/*/*.xml
configuration:
jdbc-type-for-null: null
global-config:
# 关闭mybatis-plus的banner
banner: false

MybatisPlusConfig.java

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@MapperScan(value = {"top.jasonkayzk.ezshare.*.dao.mapper"})
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}

配置说明:

  • type-aliases-package: Model类存放处, 对象映射类
  • mapper-locations: mapper.xml配置存放位置
  • MapperScan: Mapper接口类存放位置
  • PaginationInterceptor: 分页插件

另外还需要加入依赖, 见: 关于Mybatis-plus调用baseMapper报错Invalid-bound-statement的解决


② Swagger配置

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Swagger相关配置
swagger:
enabled: true
title: EZShare Application API
basePackage: top.jasonkayzk.ezshare
basePath=/**:
description: upload, download, share, file-sharing system
version: 1.0
author: Jasonkay
url: https://github.com/JasonkayZK/EZShare
email: jasonkayzk@gmail.com
license: Apache 2.0
licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.html
exclude-path: error, /ops/**

SwaggerConfig.java

1
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@Configuration
@EnableSwagger2
public class SwaggerConfig {

@Value("${swagger.enabled}")
private boolean enabled;

@Value("${swagger.basePackage}")
private String basePackage;

@Value("${swagger.title}")
private String title;

@Value("${swagger.description}")
private String description;

@Value("${swagger.version}")
private String version;

@Value("${swagger.author}")
private String author;

@Value("${swagger.url}")
private String url;

@Value("${swagger.email}")
private String email;

@Value("${swagger.license}")
private String license;

@Value("${swagger.licenseUrl}")
private String licenseUrl;

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.enable(enabled)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage(basePackage))
.paths(PathSelectors.any())
.build()
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}

/**
* 项目信息
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(title)
.description(description)
.contact(new Contact(author, url, email))
.termsOfServiceUrl(url)
.license(license)
.licenseUrl(licenseUrl)
.version(version)
.build();
}

/**
* 配置认证模式
*/
private List<ApiKey> securitySchemes() {
return newArrayList(new ApiKey("Authorization", "Authorization", "header"));
}

/**
* 配置认证上下文
*/
private List<SecurityContext> securityContexts() {
return newArrayList(SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.any())
.build());
}

private List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return newArrayList(new SecurityReference("Authorization", authorizationScopes));
}
}

更多详情见: SpringBoot集成Swagger


③ 缓存配置

application.yml

1
2
3
4
5
6
7
8
9
10
11
redis:
host: 127.0.0.1
port: 6379
password:
jedis:
pool:
min-idle: 8
max-idle: 500
max-active: 2000
max-wait: 10000
timeout: 0

RedisConfig.java

1
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
@Configuration
public class RedisConfig {

@Value("${spring.redis.host}")
private String host;

@Value("${spring.redis.port}")
private int port;

@Value("${spring.redis.password}")
private String password;

@Value("${spring.redis.jedis.pool.min-idle}")
private int minIdle;

@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;

@Value("${spring.redis.jedis.pool.max-active}")
private int maxActive;

@Value("${spring.redis.jedis.pool.max-wait}")
private long maxWaitMillis;

@Value("${spring.redis.timeout}")
private int timeout;

@Value("${spring.redis.database:0}")
private int database;

/**
* Jedis连接池配置
*
* @return JedisPool
*/
@Bean
public JedisPool redisPoolFactory() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();

jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setMaxTotal(maxActive);
jedisPoolConfig.setMinIdle(minIdle);

return StringUtils.isNotBlank(password) ?
new JedisPool(jedisPoolConfig, host, port, timeout, password, database)
:
new JedisPool(jedisPoolConfig, host, port, timeout, null, database);
}

/**
* Jedis连接配置: Standalone, Sentinel或RedisCluster
*
* @return JedisConnectionFactory
*/
@Bean(name = "redisConnectionFactory")
public JedisConnectionFactory jedisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setPassword(RedisPassword.of(password));
redisStandaloneConfiguration.setDatabase(database);

JedisClientConfiguration.JedisClientConfigurationBuilder jedisClientConfiguration = JedisClientConfiguration.builder();
jedisClientConfiguration.connectTimeout(Duration.ofMillis(timeout));
jedisClientConfiguration.usePooling();
return new JedisConnectionFactory(redisStandaloneConfiguration, jedisClientConfiguration.build());
}

@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

/**
* 自定义redis序列化的机制,重新定义一个ObjectMapper.防止和MVC的冲突
*
* @return RedisSerializer
*/
@Bean
public RedisSerializer<Object> redisSerializer() {
ObjectMapper objectMapper = new ObjectMapper();

//反序列化时候遇到不匹配的属性并不抛出异常
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

//序列化时候遇到空对象不抛出异常
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

//反序列化的时候如果是无效子类型,不抛出异常
objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);

//不使用默认的dateTime进行序列化,
objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);

//使用JSR310提供的序列化类,里面包含了大量的JDK8时间序列化类
objectMapper.registerModule(new JavaTimeModule());

//启用反序列化所需的类型信息,在属性中添加@class
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

//配置null值的序列化器
GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null);

return new GenericJackson2JsonRedisSerializer(objectMapper);
}

@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory, RedisSerializer<Object> redisSerializer) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setDefaultSerializer(redisSerializer);
template.setValueSerializer(redisSerializer);
template.setHashValueSerializer(redisSerializer);
template.setKeySerializer(StringRedisSerializer.UTF_8);
template.setHashKeySerializer(StringRedisSerializer.UTF_8);
template.afterPropertiesSet();
return template;
}

}

CacheConfig.java

1
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {

@Value("${ezshare.cache.prefix}")
private String cachePrefix;

/**
* 缓存管理器, 选择redis作为默认缓存工具
*
* @return CacheManager
*/
@Bean
public CacheManager cacheManager(@Qualifier("redisConnectionFactory") RedisConnectionFactory factory, RedisSerializer<Object> redisSerializer) {
return RedisCacheManager.builder(factory)
.cacheDefaults(getRedisCacheConfigurationWithTtl(redisSerializer))
.build();
}

private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(RedisSerializer<Object> redisSerializer) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration
.prefixKeysWith(cachePrefix)
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.entryTtl(Duration.ofMinutes(60));

return redisCacheConfiguration;
}

/**
* Key生成策略
* <p>
* 当没有指定缓存的 key时来根据类名、方法名和方法参数来生成key
*
* @return KeyGenerator
*/
@Bean
public KeyGenerator wiselyKeyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName())
.append(':')
.append(method.getName());
if (params.length > 0) {
sb.append('[');
for (Object obj : params) {
if (obj != null) {
sb.append(obj.toString());
}
}
sb.append(']');
}
return sb.toString();
};
}

}

配置说明:

  • RedisConfig: 主要进行Jedis连接, 连接池配置, 以及Redis的序列化和模板配置
  • CacheConfig: 主要进行Spring Cache相关的配置, 包括: 缓存管理器, 键生成策略, 缓存前缀, 连接管理(重试等)

④ 异步线程池配置

AsyncExecutorPoolConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class AsyncExecutorPoolConfig extends AsyncConfigurerSupport {

@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(30);
executor.setThreadNamePrefix("asyncTaskExecutor-");

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}

}

通用工具类

类名 说明 注释
ApplicationUtil 应用整体 应用整体工具类
SortUtil 封装Mybatis-Plus分页策略的排序 针对分页请求和Wrapper包装类
QueryRequest 分页请求实体类 对pageSize, pageNum, sortField和sortOrder等分页参数的包装POJO
TimeConverter 时间日期转换工具类 Excel导出时间类型字段格式化
CronValidator Cron表达式校验类 校验是否为合法的Cron表达式
BaseController 通用Controller类 用在存在分页场景下(返回分页信息)

Dict业务代码测试

Entity实体类

1
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* @author Jasonkay
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_dict")
@Excel("应用字典表")
public class Dict implements Serializable {

private static final long serialVersionUID = 285231985684474002L;

/**
* 字典ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;

/**
* 键
*/
@NotBlank(message = "{required}")
@Size(max = 10, message = "{noMoreThan}")
@ExcelField(value = "键")
private String dictKey;

/**
* 值
*/
@NotBlank(message = "{required}")
@Size(max = 20, message = "{noMoreThan}")
@ExcelField(value = "值")
private String dictValue;

/**
* 字段名称
*/
@NotBlank(message = "{required}")
@Size(max = 20, message = "{noMoreThan}")
@ExcelField(value = "字段名")
private String fieldName;

/**
* 表名
*/
@NotBlank(message = "{required}")
@Size(max = 20, message = "{noMoreThan}")
@ExcelField(value = "表名")
private String tableName;

/**
* 创建时间
*/
@ExcelField(value = "创建时间", writeConverter = TimeConverter.class)
private LocalDateTime createTime;

/**
* 修改时间
*/
@ExcelField(value = "修改时间", writeConverter = TimeConverter.class)
private LocalDateTime modifyTime;

/**
* 做数据库时间段的查询字段
*/
private transient String timeFrom;
private transient String timeTo;

}

Mapper映射类

DictMapper.java和DictMapper.xml均为代码生成

具体见: EZShare项目总结-2


Service业务类

IDictService.java

1
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
41
42
43
44
45
46
47
/**
* @author Jasonkay
*/
public interface IDictService extends IService<Dict> {

/**
* 分页查询字典
*
* @param request 分页请求
*
* @param dict 查询字典实体
*
* @return 字典列表
*/
IPage<Dict> findDicts(QueryRequest request, Dict dict);

/**
* 根据Id查询Dict
*
* @param id 字典的Id
*
* @return Dict
*/
Dict findDictById(Long id);

/**
* 插入字典
*
* @param dict 字典实体
*/
void createDict(Dict dict);

/**
* 更新字典
*
* @param dict 字典实体
*/
void updateDict(Dict dict);

/**
* 根据Id批量删除字典
*
* @param dictIds 字典Id列表
*/
void deleteDicts(String[] dictIds);

}

DictServiceImpl.java

1
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* @author Jasonkay
*/
@Slf4j
@Service("dictService")
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class DictServiceImpl extends ServiceImpl<DictMapper, Dict> implements IDictService {

@Override
public IPage<Dict> findDicts(QueryRequest request, Dict dict) {
try {
LambdaQueryWrapper<Dict> queryWrapper = new LambdaQueryWrapper<>();

if (StringUtils.isNotBlank(dict.getDictKey())) {
queryWrapper.eq(Dict::getDictKey, dict.getDictKey());
}
if (StringUtils.isNotBlank(dict.getDictValue())) {
queryWrapper.eq(Dict::getDictValue, dict.getDictValue());
}
if (StringUtils.isNotBlank(dict.getTableName())) {
queryWrapper.eq(Dict::getTableName, dict.getTableName());
}
if (StringUtils.isNotBlank(dict.getFieldName())) {
queryWrapper.eq(Dict::getFieldName, dict.getFieldName());
}

Page<Dict> page = new Page<>();
SortUtil.handlePageSort(request, page, true);

return this.page(page, queryWrapper);
} catch (Exception e) {
log.error("获取字典信息失败", e);
return null;
}
}

@Override
public Dict findDictById(Long id) {
return this.baseMapper.selectById(id);
}

@Override
@Transactional
public void createDict(Dict dict) {
this.baseMapper.insert(dict);
}

@Override
@Transactional
public void updateDict(Dict dict) {
this.baseMapper.updateById(dict);
}

@Override
@Transactional
@CacheEvict(value = "dicts", allEntries = true)
public void deleteDicts(String[] dictIds) {
this.baseMapper.deleteBatchIds(Arrays.asList(dictIds));
}

}

Controller视图控制器

DictController.java

1
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/**
* @author Jasonkay
*/
@Slf4j
@Validated
@RestController
@RequestMapping("/system/dict")
public class DictController extends BaseController {

private String logMessage;

private final IDictService dictService;

public DictController(IDictService dictService) {
this.dictService = dictService;
}

@GetMapping
// @RequiresPermissions("dict:view")
public Map<String, Object> getDictList(QueryRequest request, Dict dict) {
return getDataTable(this.dictService.findDicts(request, dict));
}

@GetMapping("/{id}")
// @RequiresPermissions("dict:select")
public Dict getDict(@PathVariable Long id) {
return this.dictService.findDictById(id);
}

@Log("新增字典")
@PostMapping
// @RequiresPermissions("dict:add")
public void addDict(@Valid Dict dict) throws EzShareException {
try {
this.dictService.createDict(dict);
} catch (Exception e) {
logMessage = "新增字典失败";
log.error(logMessage, e);
throw new EzShareException(logMessage);
}
}

@Log("修改字典")
@PutMapping
// @RequiresPermissions("dict:update")
public void updateDict(@Valid Dict dict) throws EzShareException {
try {
this.dictService.updateDict(dict);
} catch (Exception e) {
logMessage = "修改字典失败";
log.error(logMessage, e);
throw new EzShareException(logMessage);
}
}

@Log("删除字典")
@DeleteMapping("/{dictIds}")
// @RequiresPermissions("dict:delete")
public void deleteDicts(@NotBlank(message = "{required}") @PathVariable String dictIds) throws EzShareException {
try {
String[] ids = dictIds.split(StringPool.COMMA);
this.dictService.deleteDicts(ids);
} catch (Exception e) {
logMessage = "删除字典失败";
log.error(logMessage, e);
throw new EzShareException(logMessage);
}
}

@PostMapping("excel")
// @RequiresPermissions("dict:export")
public void export(QueryRequest request, Dict dict, HttpServletResponse response) throws EzShareException {
try {
List<Dict> dicts = this.dictService.findDicts(request, dict).getRecords();
ExcelKit.$Export(Dict.class, response).downXlsx(dicts, false);
} catch (Exception e) {
logMessage = "导出Excel失败";
log.error(logMessage, e);
throw new EzShareException(logMessage);
}
}

}

说明:

Mapper, Service分别继承了BaseMapper和ServiceImpl实现了基本的CRUD方法

无mapper.xml也可直接进行单表操作


测试

通过IDEA的REST Client插件进行CRUD测试和Excel导出测试, 并看到Redis中成功加入K-V, 没问题~


总结

因为一个Mybatis-Plus的依赖没有添加, 浪费了很长时间, 加上大年初一头脑也不太清晰, 不过最终还是完成~


说明: 由于缓存暂时不需要, 代码中未体现, 可通过在业务方法加入@Cacheable注解测试



本文作者:Jasonkay
本文链接:https://jasonkayzk.github.io/cn/2020/01/25/EZShare%E9%A1%B9%E7%9B%AE%E6%80%BB%E7%BB%93-3/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可