Knife4j/Swagger显示自定义枚举类型注释

需求

Knife4j/Swagger 自定义枚举类型的注释默认为枚举常量。拿AppType为例。

1
2
3
4
5
6
7
8
9
10
public enum AppType implements PersistEnum<Integer> {

ALI(1, "阿里云"),
YI_DONG(2, "移动云"),
LIAN_TONG(3, "联通云");

private Integer code;
private String desc;
...
}

默认会显示 可用值:ALI,YI_DONG,LIAN_TONG。
希望显示清楚自定义枚举的相关内容,如 1、阿里云,2、移动云,3、联通云。

解决

网络上搜索到的基本只能解决json请求及返回值的自定义,请求VO/POJO内的自定义枚举无法自定义显示,查看源码后进行编码,实现全部入参、返回值自定义枚举注释按需显示。对Swagger进行扩展,具体配置如下。

1、添加自定义注解类

1
2
3
4
5
6
7
8

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SwaggerDisplayEnum {
String code() default "code";
String desc() default "desc";
}

2、自定义注解上加上SwaggerDisplayEnum注解,如下:

1
2
3
4
5
6
7
8
9
10
@SwaggerDisplayEnum
public enum AppType {

ALI(1, "阿里云"),
YI_DONG(2, "移动云"),
LIAN_TONG(3, "联通云");

private Integer code;
private String desc;
}

本项目code,desc使用注解默认值,具体按需配置注解参数 @SwaggerDisplayEnum(code=”xxx”, desc=”yyy”)。

3、添加Swagger扩展配置

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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
@Configuration
@Slf4j
public class SwaggerEnumPlugin implements ModelPropertyBuilderPlugin, ParameterBuilderPlugin, OperationBuilderPlugin, ExpandedParameterBuilderPlugin {

private static final Joiner joiner = Joiner.on(",");

@Override
public void apply(ModelPropertyContext context) {
Optional<BeanPropertyDefinition> optional = context.getBeanPropertyDefinition();
if (!optional.isPresent()) {
return;
}

Class<?> fieldType = optional.get().getField().getRawType();
addDescForEnum(context, fieldType);
}

private void addDescForEnum(ModelPropertyContext context, Class<?> fieldType) {
if (Enum.class.isAssignableFrom(fieldType)) {
SwaggerDisplayEnum anno = AnnotationUtils.findAnnotation(fieldType, SwaggerDisplayEnum.class);
if (anno != null) {
Object[] enumConstants = fieldType.getEnumConstants();
List<String> displayValues = getDisplayValues(anno, enumConstants);

ModelPropertyBuilder builder = context.getBuilder();
Field descField = ReflectionUtils.findField(builder.getClass(), "description");
ReflectionUtils.makeAccessible(descField);
String joinText = (ReflectionUtils.getField(descField, builder) == null ? "" : (ReflectionUtils.getField(descField, builder) + ":"))
+ joiner.join(displayValues);

builder.description(joinText).type(context.getResolver().resolve(Integer.class));
}
}
}

@Override
public void apply(OperationContext context) {
Map<String, List<String>> map = new HashMap<>();
List<ResolvedMethodParameter> parameters = context.getParameters();
parameters.forEach(parameter -> {
ResolvedType parameterType = parameter.getParameterType();
Class<?> clazz = parameterType.getErasedType();
if (Enum.class.isAssignableFrom(clazz)) {
SwaggerDisplayEnum annotation = AnnotationUtils.findAnnotation(clazz, SwaggerDisplayEnum.class);
if (annotation != null) {
Object[] enumConstants = clazz.getEnumConstants();
List<String> displayValues = getDisplayValues(annotation, enumConstants);
map.put(parameter.defaultName().orElse(""), displayValues);

OperationBuilder operationBuilder = context.operationBuilder();
Field parametersField = ReflectionUtils.findField(operationBuilder.getClass(), "parameters");
ReflectionUtils.makeAccessible(parametersField);
List<Parameter> list = (List<Parameter>) ReflectionUtils.getField(parametersField, operationBuilder);

map.forEach((k, v) -> {
for (Parameter currentParameter : list) {
if (StringUtils.equals(currentParameter.getName(), k)) {
Field description = ReflectionUtils.findField(currentParameter.getClass(), "description");
ReflectionUtils.makeAccessible(description);
Object field = ReflectionUtils.getField(description, currentParameter);
ReflectionUtils.setField(description, currentParameter, field + ":" + joiner.join(v));
break;
}
}
});
}
}
});
}

@Override
public void apply(ParameterContext context) {
Class<?> type = context.resolvedMethodParameter().getParameterType().getErasedType();
ParameterBuilder parameterBuilder = context.parameterBuilder();
setAvailableValue(parameterBuilder, type);
}

private void setAvailableValue(ParameterBuilder parameterBuilder, Class<?> type) {
if (Enum.class.isAssignableFrom(type)) {
SwaggerDisplayEnum annotation = AnnotationUtils.findAnnotation(type, SwaggerDisplayEnum.class);
if (annotation != null) {
String code = annotation.code();
Object[] enumConstants = type.getEnumConstants();
List<String> displayValues = Arrays.stream(enumConstants).filter(Objects::nonNull).map(item -> {
Class<?> currentClass = item.getClass();

Field codeField = ReflectionUtils.findField(currentClass, code);
assert codeField != null;
ReflectionUtils.makeAccessible(codeField);
Object codeStr = ReflectionUtils.getField(codeField, item);
assert codeStr != null;
return codeStr.toString();

}).collect(Collectors.toList());

// 设置可用值
AllowableListValues values = new AllowableListValues(displayValues, "LIST");
parameterBuilder.allowableValues(values);
}
}
}

@Override
public void apply(ParameterExpansionContext context) {
Class<?> type = context.getFieldType().getErasedType();
ParameterBuilder parameterBuilder = context.getParameterBuilder();
if (Enum.class.isAssignableFrom(type)) {
setAvailableValue(parameterBuilder, type);
SwaggerDisplayEnum annotation = AnnotationUtils.findAnnotation(type, SwaggerDisplayEnum.class);
if (annotation != null) {
Object[] enumConstants = type.getEnumConstants();
List<String> displayValues = getDisplayValues(annotation, enumConstants);

Field descField = ReflectionUtils.findField(parameterBuilder.getClass(), "description");
ReflectionUtils.makeAccessible(descField);
String joinText = (ReflectionUtils.getField(descField, parameterBuilder) == null ? "" : (ReflectionUtils.getField(descField, parameterBuilder) + ":"))
+ joiner.join(displayValues);

parameterBuilder.description(joinText);
}
}
}

private List<String> getDisplayValues(SwaggerDisplayEnum annotation, Object[] enumConstants) {
if (annotation == null) {
return Lists.newArrayList();
}
String code = annotation.code();
String desc = annotation.desc();
return Arrays.stream(enumConstants).filter(Objects::nonNull).map(
item -> {
Class<?> currentClass = item.getClass();
Field codeField = ReflectionUtils.findField(currentClass, code);
assert codeField != null;
ReflectionUtils.makeAccessible(codeField);
Object codeStr = ReflectionUtils.getField(codeField, item);

Field descField = ReflectionUtils.findField(currentClass, desc);
assert descField != null;
ReflectionUtils.makeAccessible(descField);
Object descStr = ReflectionUtils.getField(descField, item);

return codeStr + "、" + descStr;
}
).collect(Collectors.toList());
}

@Override
public boolean supports(DocumentationType documentationType) {
return true;
}
}

其中,
ModelPropertyBuilderPlugin 适用于 application/json 提交的请求参数 及 json返回值 注释
ParameterBuilderPlugin、OperationBuilderPlugin 适用于请求方法上的直接枚举参数
ExpandedParameterBuilderPlugin 适用于POJO/VO内的枚举参数(网络上搜索到前几篇内容未实现的部分)

5、最终效果
Knife4j/Swagger自定义枚举1

Knife4j/Swagger自定义枚举2

SpringMVC访问Maven子依赖jar下的静态资源

需求

最近在设计编码存储(云存储、本地存储)管理系统,希望做到封装独立模块jar,jar内包含功能代码及前端打包后的代码。使用时创建另外的maven web项目依赖该jar,web项目可访问依赖jar内的静态资源html、js、css等。

解决

之前项目没有遇到过该场景,尝试检索“spring访问依赖jar静态资源”等各类关键字找不到简单明了的解决方案,后在spring官网文档通过搜索jar找到关键字webjars,又找到关键的一句“servlet3.0及以上版本可以读取jar包下/META-INF/resources资源”,最终测试成功。

结合实际业务简化如下:

1、maven多模块项目,使用了Spring Boot,模块A为功能模块,不需要可执行,模块A中有前端代码;模块B依赖模块A,作为可执行jar,运行后可通过访问localhost:8080打开网页。

2、在模块A的resources/static目录下放置前端编译好的代码结构如下。

依赖jar里前端代码的位置

3、模块A中配置resources插件,将resources代码打包至jar包的META-INF/resources中。

1
2
3
4
5
6
7
8
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<targetPath>META-INF/resources</targetPath>
</resource>
</resources>
</build>

4、模块B依赖模块A,同时在application.yml中进行配置。

1
2
3
4
5
<dependency>
<groupId>com.test</groupId>
<artifactId>oss-test</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
1
2
3
spring:
resources:
static-locations: 'classpath:/META-INF/resources/static/'

5、启动模块B,页面访问成功。

Sharding-jdbc错误:Failed to configure a DataSource

问题

Spring Boot项目集成Sharding-jdbc, 配置的是单库分表,正常添加了pom、配置文件。其中sharding-sphere使用了当前最新的版本: 4.1.1

1
2
3
4
5
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
shardingsphere:
datasource:
names: master
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xxx:3306/xxx
username: xx
password: xx
sharding:
tables:
st:
key-generator:
column: id
type: SNOWFLAKE
actual-data-nodes: master.st_$->{0..3}
table-strategy:
inline:
sharding-column: st_id
algorithm-expression: st_$->{st_id % 4}

启动后报数据源相关错误
Failed to configure a DataSource: ‘url’ attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class

Sharding-jdbc url attribute is not specified

解决

参考网上一般的解决方案 升级POM版本 没有效果,看Issues( https://github.com/apache/shardingsphere/issues/3831 )里有提到排除相关boot-starter。

尝试将原先使用的pom

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>xxx</version>
</dependency>

改为

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.5</version>
</dependency>

成功启动。

Java使用Vlcj进行屏幕录制

需求

使用Java编写GUI程序完成简单的屏幕录制操作,支持开始录屏、结束录屏、销毁本次录制等操作。

解决

添加主要依赖

1
2
3
4
5
<dependency>
<groupId>uk.co.caprica</groupId>
<artifactId>vlcj</artifactId>
<version>4.2.0</version>
</dependency>

关键代码

1、Application.java(项目的入口)

1
2
3
4
5
6
public class Application {
public static void main(String[] args) {
// 其中 destination 为 录制完成后视频文件存放的目录
SwingUtilities.invokeLater(() -> new ScreenRecorder(destination));
}
}

2、ScreenRecorder.java(GUI 及 录屏 主要逻辑)

程序 包含3个按钮,开始、停止、销毁;2个操作目录,录制完成后视频文件存放的目录A(vlcj使用) 及 待处理业务目录B(自身业务使用)。

一般流程是 先点击 开始,此时 目录A下会 多一个视频文件,录制一段视频,点击停止,程序会将目录A下的视频移至 目录B下,此时可进行后续业务处理。因为 开始录制时目录A就会创建文件,导致目录A下文件可能不是最终文件(录制未完成),所以目录B就有存在的意义,程序关闭再启动后也可遍历 目录B下的文件 处理未完成的业务。

销毁按钮用于 开始录制但未停止时想放弃本次录制,注意停止后再销毁无效。

录制中关闭程序,则保存本次录制。

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
public class ScreenRecorder {

private static final String[] OPTIONS = {
"--quiet",
"--quiet-synchro",
"--intf",
"dummy"
};

private static String filePath = "";

private static final String MRL = "screen://";
private static final String SOUT = ":sout=#transcode{vcodec=mp4v,acodec=mpga,vb=4096,ab=512}:duplicate{dst=file{dst=%s}}";
private static final String FPS = ":screen-fps=20";
private static final String CACHING = ":screen-caching=500";

private final MediaPlayerFactory mediaPlayerFactory;
private final MediaPlayer mediaPlayer;

private JFrame frame;
private static JLabel countLabel;
private static JTextField nameField;

public ScreenRecorder(final String destination) {
mediaPlayerFactory = new MediaPlayerFactory(OPTIONS);
mediaPlayer = mediaPlayerFactory.mediaPlayers().newMediaPlayer();
JPanel cp = new JPanel();
nameField = new JTextField(5);
JButton recordButton = new JButton("开始");
JButton stopButton = new JButton("停止");
JButton delButton = new JButton("销毁");
cp.add(recordButton);
cp.add(stopButton);
cp.add(delButton);
frame = new JFrame("录屏程序");
// 图标化
// frame.setExtendedState(JFrame.ICONIFIED);
frame.setContentPane(cp);
frame.setLocation(10, 10);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
release();
}
});
recordButton.addActionListener(e -> go(destination));
stopButton.addActionListener(e -> stop());
delButton.addActionListener(e -> delete());
frame.setVisible(true);
}

private void go(String destination) {
State state = mediaPlayer.status().state();
filePath = destination + getUUID() + ".mp4";
mediaPlayer.media().play(MRL, getMediaOptions(filePath));
updateTitle("录屏中");
}

private void stop() {
State state = mediaPlayer.status().state();
mediaPlayer.controls().stop();
if (!state.equals(State.STOPPED) && !state.equals(State.NOTHING_SPECIAL)) {
// 将 目录A下的文件 移动到 目录B
FileUtil.moveFile(filePath, waitSendPath);
filePath = "";
}
updateTitle("停止");
}

private void delete() {
mediaPlayer.controls().stop();
if (!"".equals(filePath)) {
FileUtil.delFile(filePath);
}
updateTitle("停止");
}

private void release() {
State state = mediaPlayer.status().state();
if (state.equals(State.PLAYING)) {
// 如果关闭时程序录制中,则保存本次录制
stop();
}
log.info("status:" + state);

mediaPlayer.release();
mediaPlayerFactory.release();
}

private void updateTitle(String title) {
frame.setTitle("录屏程序:" + title);
}

private String[] getMediaOptions(String destination) {
String result = String.format(SOUT, destination);
return new String[] {
result,
FPS,
CACHING
};
}
}
注意
  1. vlcj好像不能处理好windows下的中文文件名乱码的问题(创建的中文文件名乱码),mac下没问题,可以定义map存放 目录A下的随机非中文文件名 及 想要的中文文件名,在将文件从目录A移动到目录B后使用中文文件名。
  2. 虽然视频文件是以.mp4结尾,且视频文件能用一般的播放器打开,但文件应该不是标准的mp4,使用浏览器无法直接播放。我对视频编码这里不了解,因此也没有进行后续研究(有更好的解决办法欢迎留言交流),后续业务采用阿里云进行视频转码,转换后的mp4可在浏览器中直接打开。
  3. 暂不支持声音录制。
  4. 可使用Java监听 目录B 文件变化来实现后续业务 Java监听文件变化

界面效果

Java Vlcj录屏程序最终效果

vue nuxt window is not defined 或 document is not defined错误

问题

vue nuxt 中出现 window is not defined 或 document is not defined 错误。

原因

vue服务端渲染ssr使用了客户端的一些对象,即代码中使用了window对象或document对象,可能是由于引入的第三方组件中操作了这些对象,也可能是自己的代码使用了它们。

解决

1、由于引入部分第三方组件导致的错误可以通过配置插件ssr为false来解决。

1
2
3
4
5
6
7
...
plugins: [
...
{ src: '~/plugins/xxx', ssr: false },
...
]
...

2、由于自己操作window对象在网上找了半天都没有解决办法
基本都是第三方组件的方案或者是官网上下面的无效方案

1
2
3
4
// 截止2019-06-02 亲测无效
if (process.client) {
require('external_library')
}

终于在缩小搜索时间后找到了别人的解决方案。
在.vue文件中的window上操作的代码外加一层判断,和官网的方案其实差不多,就是官网的require是什么鬼,容易误解!!!也是我自己笨!!

1
2
3
4
// 正确的使用办法
if (process.client) {
window.xxx
}

特别小的问题,花了较多时间,遂记录一下。

参考:
Nuxt中关于window or document is not defined的问题总结

npm检查依赖最新版本并升级

使用 npm-check-updates 检查依赖并升级。

1.安装 npm-check-updates

1
$ npm install npm-check-updates -g

2.查看package.json中依赖的最新版本

1
$ ncu

执行结果:

1
2
3
4
5
6
7
8
9
10
11
Using /Users/xxx/package.json
⸨░░░░░░░░░░░░░░░░░░⸩ ⠸ :
mobx ^3.3.1 → ^5.5.0
mobx-react ^4.2.2 → ^5.2.8

The following dependencies are satisfied by their declared version range, but the installed versions are behind. You can install the latest versions without modifying your package file by using npm update. If you want to update the dependencies in your package file anyway, run ncu -a.

antd ^3.1.2 → ^3.10.1
node-sass ^4.6.1 → ^4.9.3

Run ncu with -u to upgrade package.json

3.更新package.json依赖到最新版本

1
$ ncu -a 

4.执行npm install 完成新依赖安装升级

1
$ npm install

5.完成升级

注意前端发展日新月异,版本间有时差距较大,不建议做无准备的升级。

Linux系统用户权限命令小记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建用户组 dev
$ groupadd dev

# root用户 添加 附加组dev
$ usermod -G dev root

# 指定目录或文件 删除其他人read权限
$ sudo chmod o-r config

# 指定目录或文件拥有组改为 dev
$ sudo chown dev account/

# 创建test用户并添加到 dev用户组
$ useradd -g dev test

# 更新test用户密码
$ passwd test

err_incomplete_chunked_encoding错误

问题

项目使用ajax请求json数据大量返回值下,浏览器可能会报 err_incomplete_chunked_encoding 错误。

解决

项目使用了nginx,在大量返回值的情况下,nginx需要使用到临时文件,有的时候上述错误是由 nginx权限不足导致的,可参考nginx日志配置对应目录权限。

本项目由于返回值过大,需更新nginx配置,proxy_max_temp_file_size设个合适的较大的值,或直接设为0,关闭硬盘缓冲。

1
2
3
server {
proxy_max_temp_file_size 0;
}

问题解决。

禁止Chrome强制Https跳转

问题

场景1:
网站原先使用了https证书,现在不再使用,chrome打开网站会进行https跳转,导致异常。

场景2:
主域名配置单个https证书,其二级域名未配置正式,chrome可能会对二级域名进行https跳转。

此时chrome会有如下提示:

https跳转提示您的连接不上私密连接

解决

chrome的地址栏输入:

1
chrome://net-internals/#hsts

Delete domain security policies 中输入 不想自动https跳转的地址,然后点击“delete”按钮,即可完成配置。

chrome删除https跳转

当然这个配置的前提是 网站原来就支持http且未开启https强制跳转,主要可以解决主域名有https证书但二级域名没有,访问二级域名自动跳转https的问题,像本站 blog.gelu.me,做了https强制跳转,就算删除后http请求也会再次重定向到https。

mysql性能优化-limit分页

问题

平台在主数据量不大的情况下遇到了MySQL limit较大偏移量分页查询极慢的情况。

分析解决

该查询语句存在的一些问题

问题1:

查询使用的 select *,虽然知道从性能角度来说应该用什么查什么,但项目非大数据类的项目,可见项目生命周期内也不会有超大量数据,因此为加快开发效率还是使用了select *。

问题2:

表中存在部分大字段,limit m, n 使得结果集查询了 m + n 的数据,并将 m 以前的数据抛弃。配合第一个问题,导致了 m 较大时查询非常慢。

实际项目中,这些字段当前也没有了使用需求,因此予以删除,基本解决了查询慢的问题。同时该表字段过多,近100个,会在后续的改版优化中进行拆分。

因为遇到了分页查询慢的问题,顺便整理收集了一下真正大量数据下的分页优化方案。

解决方案

1
select * from table_name inner join ( select id from table_name where xxx=yyy limit 200000,10) b using (id)

还有一些缩减查询范围 和 使用子查询的一些方法,有诸多限制,因此就没有记录。

当然有些时候大量数据查询场景可以避免,上家公司千万级的数据就通过归档数据减少数据库压力,也可以缩减业务查询范围避免大量数据查询。