hello云胜

技术与生活

0%

今天发现项目启动时,会打印一堆报错。吓了一跳。

查看之后发现是swagger的错误提示。并不影响功能。但是看着一堆错误,也闹心不是?

所以还是要解决一下。

1
2
3
4
5
6
2020-11-04 10:50:46,383 [http-nio-9292-exec-3] WARN  i.s.m.parameters.AbstractSerializableParameter:421 - Illegal DefaultValue null for parameter type integer
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.valueOf(Long.java:803)
at io.swagger.models.parameters.AbstractSerializableParameter.getExample(AbstractSerializableParameter.java:412)

通过查看AbstractSerializableParameter412行的源码

1
2
if (BaseIntegerProperty.TYPE.equals(type)) {
return Long.valueOf(example);

发现如果属性类型是Integer,那么就转成Long

而example默认为””,导致转换错误。

第一种做法

所以,得到解决方法1:

实体类中,Integer类型的属性加@ApiModelProperty时,必须要给example参数赋值,且值必须为数字类型。

1
2
@ApiModelProperty(value = "", example = "0")
private Integer id;

但是,如果项目中有大量的修改点,那改动起来就太累了。

第二种做法

然后再看这个代码

1
2
3
if (example == null) {
return null;
}

在前面有个null判断,如果加个“”空字符串判断其实也就ok了

但是,这需要我们能下载swagger的源码,自己打包发布。

然后替换掉官方版本

第三种做法

这可以算是swagger的一个小bug,在网上看到说这个问题是swagger-models:1.5.20的bug。

看了下我这确实是这个版本。

image-20201104161808581

1.5.21版本已经修改了这个问题,所以我们也可以通过替换依赖解决

swagger-models是springfox-swagger2依赖进来的

1
2
3
4
5
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>

查看了下他的依赖,是1.5.20版本

image-20201104162257662

然后查看了最新的3.0.0版本的springfox-swagger2,依赖的依然是1.5.20的swagger-models

所以只能手动指定swagger-models的版本。话说回来即使高版本的swagger2依赖了1.5.21的models,我也不敢直接升级springfox-swagger2,毕竟这动作有点大。需要做全面的回归测试才敢发生产。

1
2
3
4
5
6
7
8
9
10
11
<!--Swagger-UI API文档生产工具-->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>

通过在springfox-swagger2依赖之前,手工加上swagger-models1.5.21版本的依赖

根据maven的依赖原则,同路径长度下,谁先声明谁优先,把1.5.21放在上面,即可排除springfox-swagger2依赖的1.5.20版本

不需要手动加exclusions

image-20201104163824278

推荐第三种做法。我这边发版测试ok。

用java打tar包

写了一个打tar包的工具类,可以递归打整个目录。没有中文文件名的问题。

引入依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.20</version>
</dependency>

代码:

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
public class TarUtil {
private static List<File> files = new ArrayList<File>();
public static final String[] paths = new String[]{
"D:\\图库",
"D:\\y.jpg",
"D:\\一个合同.pdf"
};
public static final File target = new File("D:\\打包测试.tar");

/**
* @return File 返回打包后的文件
* @throws
* @Title: pack
* @Description: 将一组文件打成tar包
*/
public static File pack() {
long startTime = System.currentTimeMillis();
FileOutputStream out = null;
try {
out = new FileOutputStream(target);
} catch (FileNotFoundException e1) {
e1.printStackTrace();
}
TarArchiveOutputStream os = new TarArchiveOutputStream(out);
os.setLongFileMode(2);
System.out.println("***************开始打" + target + "包****************");
int fileCount = 0;
for (File file : getSourcesFile()) {
try {
os.putArchiveEntry(new TarArchiveEntry(file));
IOUtils.copy(new FileInputStream(file), os);
os.closeArchiveEntry();
fileCount++;
} catch (FileNotFoundException e) {
System.out.print("****创建" + file.getName() + "******异常");
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
if (os != null) {
try {
os.flush();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
System.out.println("***************打" + target + "包结束:用时 "
+ (endTime - startTime) + "毫秒,共" + fileCount + "个文件。");

return target;
}

public static List<File> getSourcesFile() {

for (int i = 0; i < paths.length; i++) {
try {
File file = new File(paths[i]);
getFilesByDirectory(file);
} catch (Exception e) {
System.out.println("-----创建文件" + paths[i] + "异常------");
e.printStackTrace();
}
}
return files;
}

public static void getFilesByDirectory(File file) {
if (file.isDirectory()) {
File[] fileArrays = file.listFiles();
for (File f : fileArrays) {
getFilesByDirectory(f);
}
} else {
files.add(file);
}
}

public static void main(String[] args) {
pack();
}
}

效果:

image-20200328153141891

源码路径:https://github.com/jedyang/demo

高性能无锁队列MpscQueue

Netty源码中大量使用MpscQueue,这是一个无锁队列,可实现多线程下高性能。Netty为什么不使用java原生队列?MpscQueue的原理是怎样的?

Java原生队列

Java原生队列按照实现方式,可分为阻塞队列和非阻塞队列两种。阻塞队列是基于锁实现的,非阻塞对立是基于CAS操作实现的。

Java并发队列

开发自己的starter

starter的好处是,集成众多依赖,提供一个一站式的依赖项。 Starter相当于模块,它能将模块所需的依赖整合起来并对模块内的Bean根据环境( 条件)进行自动配置。 使用者只需要依赖相应功能的Starter,无需做过多的配置和依赖, Spring Boot就能自动扫描并加载相应的模块。

命名

spring官方的starter命令为spring-boot-starter-xxx,所以我们开发的项目不要以spring-boot开头。 建议写成:xxx公司组织-spring-boot-starter-yyy模块

开发步骤

1.新建Maven项目,在项目的POM文件中定义使用的依赖;

2.新建配置类,写好配置项和默认的配置值,指明配置项前缀;

3.新建自动装配类,使用@Configuration和@Bean来进行自动装配;

4.新建spring.factories文件,指定Starter的自动装配类;

具体代码

1,新建一个springboot项目,在pom文件中增加以下依赖:

1
2
3
4
5
<dependency> 		
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

主要的作用是在编译时在META-INF下生成spring-configuration-metadata.json 文件,该文件主要为IDE使用。 即可以通过在application.properties文件中通过ctrl + 点击进入配置属性所在的类中

2,配置类

1
2
3
4
5
6
7
8
9
10
@ConfigurationProperties(prefix = "spring101") 
@Data
public class MyServiceProperties {
private String name;
private Integer age;
/**
* 通过版本决定使用哪个服务
*/
private String version;
}

关键是要指定配置项的前缀。这些配置项,也可以设置默认值。

这些配置项可以让starter的使用者进行配置

3,自动装配类

自动装配类是整个starter的逻辑核心。根据配置项的值,自动注入合适的bean。

在这个项目中,有一个抽象的AbstractMyService,代表客户端会使用的服务bean。

MyStarterServiceV1和MyStarterServiceV2是根据条件注入的具体实现类。

也就是客户端在配置spring101.version=v1会使用MyStarterServiceV1,

配置spring101.version=v2会使用MyStarterServiceV2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 @Configuration 
@EnableConfigurationProperties(MyServiceProperties.class)
@Slf4j
public class MyAutoConfiguration {
@Bean
// ConditionalOnMissingBean:当客户端没有自己实现的service时,使用默认实现
@ConditionalOnMissingBean(MyStarterServiceV1.class)
// 使用的控制条件
// matchIfMissing = true,则表示即使配置文件中没有定义该属性配置,也会加载该方法
@ConditionalOnProperty(prefix = "spring101", name = "version", havingValue = "v1", matchIfMissing = true)
MyStarterServiceV1 getMyService(){
return new MyStarterServiceV1("hello");
}
@Bean
@ConditionalOnMissingBean(MyStarterServiceV2.class)
@ConditionalOnProperty(prefix = "spring101", name = "version", havingValue = "v2")
MyStarterServiceV2 getMyV2Service(){
return new MyStarterServiceV2("hello");
}
}

4,新建spring.factories文件,指定Starter的自动装配类。

在resources下新建META-INF文件夹,新建spring.factories文件。内容为:

1
2
3
#指定autoconfigure加载的自动装配类是哪个

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.uncley.MyAutoConfiguration

5,打包测试 maven clean install

先install到本地仓库测试

新创建一个普通的springboot工程:mystarter-use 依赖

在application.properties增加配置

1
2
3
4
5
spring101.age=22

spring101.name=uncleY

spring101.version=v2

写一个测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component

@Slf4j

public class MyStarterUseTest implements CommandLineRunner {

@Autowired

private AbstractMyService myService;

@Override

public void run(String... args) throws Exception {

log.info(myService.hello());

}

}

通过修改spring101.version的值可以观察到,实例化了不同的service

注意:

pom文件中的

1
2
3
4
5
6
7
8
<build>      
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

要去掉,否则项目jar包的目录不合规范

JWT实践

image-20200503222600176

此时没有token

image-20200503222510637

把token填上

image-20200503224258308

测试,现在可以获取成功了

image-20200503224244244

SpringBoot + Mybatis 读写分离

代码环境是 springboot+mybatis+druib 连接池。想要读写分离就需要配置多个数据源,在进行写操作是选择写的数据源,读操作时选择读的数据源。其中有两个关键点:

  • 如何切换数据源
  • 如何根据不同的方法选择正确的数据源

自定义Mybatis Generator代码类型解析器

使用MapStruct 高效优雅的进行Bean转换

烦人的Bean 转换

对于代码中 JavaBean之间的转换, 一直是困扰我很久的事情。 在开发的时候我看到业务代码之间有很多的 JavaBean 之间的相互转化, 非常的影响观感, 却又不得不存在。 我后来想的一个办法就是通过反射, 或者自己写很多的转换器。

第一种通过反射的方法确实比较方便, 但是现在无论是 BeanUtils, BeanCopier 等在使用反射的时候都会影响到性能。 虽然我们可以进行反射信息的缓存来提高性能。 但是像这种的话, 需要类型和名称都一样才会进行映射, 有很多时候, 由于不同的团队之间使用的名词不一样, 还是需要很多的手动 set/get 等功能。

第二种的话就是会很浪费时间, 而且在添加新的字段的时候也要进行方法的修改。 不过, 由于不需要进行反射, 其性能是很高的。

MapStruct 带来的改变

MapSturct 是一个生成 类型安全, 高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。

抓一下重点:

  1. 注解处理器
  2. 可以生成 JavaBean 之间那的映射代码
  3. 类型安全, 高性能, 无依赖性

从字面的理解, 我们可以知道, 该工具可以帮我们实现 JavaBean 之间的转换, 通过注解的方式。

同时, 作为一个工具类,相比于手写, 其应该具有便捷, 不容易出错的特点。

MapStruct 入门

引入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>1.3.1.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.3.1.Final</version>
</dependency>

我现在在对接一个系统,传过来的是支付信息PayInfo。后面数据库存的是TExpensesRecords消费记录。我需要进行bean的转换。如果我一点点的写get/set真是太烦人了,无脑的体力劳动。

甚至中间还牵涉了很多类型转换,嵌套之类的繁琐操作,而我们想要的只是建立它们之间的映射关系而已。有没有一种通用的映射工具来帮我们搞定这一切。当然有而且还不少。有人说apache的BeanUtil.copyProperties可以实现,但是性能差而且容易出异常,很多规范严禁使用这种途径。以下是对几种对象映射框架的对比,大多数情况下 MapStruct 性能最高。原理类似于lombokMapStruct都是在编译期进行实现,而且基于GetterSetter,没有使用反射所以一般不存在运行时性能问题。

测试

我现在有两个类

一个是接口VO类

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
@Data
@ApiModel("支付信息")
public class PayInfo {

@ApiModelProperty("支付码")
private String payCode;

@ApiModelProperty("总金额")
private String totalMoney;

@ApiModelProperty("菜品")
private List<Food> foods;

@ApiModelProperty(value = "服务商id")
private String serviceId;

@ApiModelProperty(value = "服务商名称")
private String serviceName;

@ApiModelProperty(value = "餐厅编号")
private String restaurantCode;

@ApiModelProperty(value = "餐厅名称")
private String restaurantName;

@ApiModelProperty(value = "机器号")
private String machineCode;

@ApiModelProperty(value = "消费类型(1:食堂用餐、2:网点消费、3:自助贩卖机消费)")
private String expensesType;
}

一个是数据库DO类

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
@Data
@ApiModel(value ="TExpensesRecords", description="消费记录表")
public class TExpensesRecords implements Serializable {
@ApiModelProperty(value = "")
private Integer id;

@ApiModelProperty(value = "交易流水号(随便生成唯一编号)")
private String tradeId;

@ApiModelProperty(value = "员工号")
private String userCode;

@ApiModelProperty(value = "服务商id")
private String serviceId;

@ApiModelProperty(value = "服务商名称")
private String serviceName;

@ApiModelProperty(value = "餐厅编号")
private String restaurantCode;

@ApiModelProperty(value = "餐厅名称")
private String restaurantName;

@ApiModelProperty(value = "机器号")
private String machineCode;

@ApiModelProperty(value = "消费类型(1:食堂用餐、2:网点消费、3:自助贩卖机消费)")
private String expensesType;

@ApiModelProperty(value = "消费金额")
private BigDecimal amount;

@ApiModelProperty(value = "消费时间")
private LocalDateTime expensesDate;

可以看到,他们有一些属性是同名的。

我们一步步来写。

先写一个转换接口

image-20200513171630030

写一个测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void payInfo2ExpenseRecord() throws Exception {
PayInfo payInfo = new PayInfo();
payInfo.setPayCode("20200513_01471111_120_40_1589359038744");
payInfo.setTotalMoney("39.3");
payInfo.setServiceId("001");
payInfo.setServiceName("服务商A");
payInfo.setRestaurantCode("A001");
payInfo.setRestaurantName("餐厅A");
payInfo.setMachineCode("M001");
payInfo.setExpensesType("1");

TExpensesRecords tExpensesRecords =
PayInfoMapper.INSTANCE.payInfo2ExpenseRecord(payInfo);

log.info(tExpensesRecords.toString());
}

image-20200513171524556

可以看到,大部分相同名字的属性已经完成转换。但是名字不一致的,需要我们单独配置下。

处理不同名的属性

代码也很好理解,就是将源的payCode字段映射到目标的tradeId字段

看下测试结果

1
TExpensesRecords(id=null, tradeId=20200513_01471111_120_40_1589359038744, userCode=null, serviceId=001, serviceName=服务商A, restaurantCode=A001, restaurantName=餐厅A, machineCode=M001, expensesType=1, amount=39.3, expensesDate=null)

tradeId映射成功。而且totalMoney是String,映射成BigDecimal的amount也成功了

原理

image-20200513173632126

image-20200513173907468

原理类似于lombokMapStruct都是在编译期对接口进行实现,而且基于GetterSetter,没有使用反射所以一般不存在运行时性能问题。 类型不同,会自动进行转换。

Spring 注入的方式

上面的例子是默认的方式

1
PayInfoMapper INSTANCE = Mappers.getMapper(PayInfoMapper.class);

在正常的项目中,一般和spring整合使用

就是在 @Mapper 后面加入 componentModel=”spring”
image-20200513173150778

在用到的地方就可以使用@Autowired注入了

注解说明

1
2
3
4
5
6
7
8
9
10
11
12
@Mapper 只有在接口加上这个注解, MapStruct 才会去实现该接口
@Mapper 里有个 componentModel 属性,主要是指定实现类的类型,一般用到两个
default:默认,可以通过 Mappers.getMapper(Class) 方式获取实例对象
spring:在接口的实现类上自动添加注解 @Component,可通过 @Autowired 方式注入
@Mapping:属性映射,若源对象属性与目标对象名字一致,会自动映射对应属性
source:源属性
target:目标属性
dateFormat:String 到 Date 日期之间相互转换,通过 SimpleDateFormat,该值为 SimpleDateFormat 的日期格式
ignore: 忽略这个字段
@Mappings:配置多个@Mapping
@MappingTarget 用于更新已有对象
@InheritConfiguration 用于继承配置

image-20200810172312935

高级使用

多对一

两个源对象都有同样的字段,需要指定使用哪个

1
2
3
4
@Mappings({
@Mapping(source = "bankcardInfo.bankCardNo", target = "bankCardNo")
})
PayChannelVo buildPayChannelVo(TUserBankcardInfo bankcardInfo, TBankcardSupport bankcardSupport);

类型转换

List和String互转

需求背景:有一个业务对象,数据库里村的是分号隔开的String,在返回给前台是需要转换成List

dao类中

1
2
@ApiModelProperty(value = "附件地址,以分号隔开")
private String attachments;

vo类中

1
2
@ApiModelProperty(value = "附件地址列表")
private List<String> attachmentList;

mapStruct类中的写法

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
@Mapper(componentModel = "spring")
public interface NoticeExchange {
@Mappings({
@Mapping(source = "attachments", target = "attachmentList"),
@Mapping(source = "types", target = "typeList")
})
NoticeVo dao2Vo(TNotice dao);

@Mappings({
@Mapping(source = "attachmentList", target = "attachments"),
@Mapping(source = "typeList", target = "types")
})
TNotice vo2dao(NoticeVo vo);

// str转list
default List<String> str2List(String src){
String[] split = src.split(";");
List<String> result = Arrays.asList(split);
return result;
}

// list转str
default String list2Str(List<String> src){
if (CollUtil.isEmpty(src)) {
return "";
}
StringBuffer sb = new StringBuffer();
src.stream().forEach(item -> sb.append(item).append(";"));
return sb.toString();
}
}

当需要String转List时,MapStruct会自动调用str2List。当需要list转string时,会自动调用

list2Str