在 Junit 使用 MockMvc 对 Controller 层进行单元测试时,发现控制台打印的响应结果中文出现乱码,导致断言异常(预期值与结果不符)。
这是由于 spring 默认字符集为 ISO-8859-1,中文无法被正确的识别。因此只要能够重新设置支持中文的字符集即可解决该问题。
虽然使用 spring boot 预留的配置可以轻松的解决问题,但是粒度太粗了,一旦设置全局生效,唯一值得庆幸的是,此处需要设置的是字符集,大部分场景下都会使用 UFT-8。
1 | server: |
不要使用 MockMvcResultHandlers.print()
方法输出响应结果,重新实现 ResultHandler
接口。
1 | public class ForceCharsetPrintingResultHandler extends PrintingResultHandler { |
1 | mockMvc.perform() |
通过自定义注解,拓展 JSR 303
规范,以实现校验参数中多个字段不可同时为空的情况。
即指定字段列表中,至少一个为非空值。
等价于多个字段 @NotNull、@NotEmpty、@NotAllBlank 的或操作。
1 |
|
1 |
|
1 |
|
1 |
|
1 | 使用 @NotAllNull 的字段不能全为 null = |
@NotAllBlank
(略),至于区别:
需要使用查询结果作为参数,进行二次查询。所以需要使用脚本执行多步操作,将查询结果保存在参数中。
若需要使用变量,则必须先在外部声明。
1 | DECLARE |
1 | BEGIN |
直接使用会出现异常:
1 | PLS-00103: Encountered the symbol "ALTER" when expecting one of the following: |
需要使用立即执行命令:
1 | EXECUTE IMMEDIATE 'a SQL'; |
使用 @DateTimeFormat
可以对非 Json (即未被 @RequestBody
修饰的)参数进行反序列化,该注解由 Spring Boot 提供,与序列化工具无关。
而需要处理 Json 参数时,则必须借助 Json 序列化框架来实现。
Jackson 提供了 @JsonFormat
注解,同时用于日期的(反)序列化。但是默认情况下,并不支持 Jsr310 新增的、即 java 8 时间类,如 LocalDate。
需要添加依赖(默认继承 Jackson 提供的版本号即可):
1 | <dependency> |
此时,Spring Boot 将自动完成注册。
但是,若在项目中手动新建了 Jackson 处理器,或者重新注册了 ObjectMapper Bean,则需要在注册时,修改配置以添加支持:
1 | // 通常时在 web 配置中重新消息响应器 |
mybatis 中 使用 LocalDate 等时间类型无法被正确的映射,出现以下异常:
1 | Caused by: org.apache.ibatis.type.TypeException: Error setting non null for parameter #1 with JdbcType null . Try setting a different JdbcType for this parameter or a different configuration property. Cause: java.sql.SQLException: 无效的列类型 |
添加类型处理器依赖即可(3.4 +):
1 | <dependency> |
老版本需要手动配置:
1 | <typeHandlers> |
restTemplate 是 spring 提供的基于 restful 设计的 http 请求客户端。
springboot 默认提供了 restTemplate 相关的 Bean,但也可以通过手动注册同类型的 bean 来自定义一些配置,如通用的请求头,拦截器等。
1 |
|
一般的会将特定功能的拦截器,注册到独立的 restTemplate bean 中,即设置多个 restTemplate bean(默认同类型下只能存在一个 bean,可以使用包装类,装饰器)。
1 | // 定义自定义拦截器 |
项目中经常使用 FastJson
替换 Jackson
,实际上项目中存在一个 Json 处理工具即可,(Jackson
挺好用的,感觉 Bug 少多了)。
1 | public void setRestTemplate(RestTemplate restTemplate) { |
使用 restful 占位符,设置方式和 controller 层一致,如:
1 | void testGetParam() { |
ps:当然参数也可以使用 Map
对于任意使用 ListT[].class
替代,可以很好的避免 List<?> 无法获取具体类型的问题。
当然也可以使用 new ParameterizedTypeReference<List<T>>(){}
,但其本身是通过生成匿名类(类中的泛型不会被擦除)的方式来记录泛型信息。
使用数组类型无疑更简洁优雅。
1 | void testExchange() { |
1 | void testExecute() { |
需要对 api 编写测试类,可以采用的方式:
但是由于 MockMvc 的结果值是字符串,虽然提供了 jsonPath 方法解析,但是处理过程丧失了面向对象的优雅,可能其对于 controller 层作单元测试,结合 MockBean,应当会更方便,轻量;而在处理完整的 api 测试,使用 restTemplate 通过泛型可以直接获取到结果对象,处理过程更灵活。
ps:使用 RestTemplate 是需要获取端口号的。
在启用 @SpringBootTest
时,无法直接从配置文件读取端口号,需要指定 :
1 |
否则会出现 -1 或 null 的情况,之后可以通过常规的 spring 注入方式,来获取端口号,如:
1 | // 1. server.port 或 local.server.port |
期间也是测试了 MockMvc 的使用,需要 @AutoConfigureMockMvc
注解在测试类上,启用相关配置。
1 |
|
但使用过程并不顺利,遇到了以下异常:
1 | org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.NullPointerException |
通过异常信息,只能获取到空指针异常,无法直接定位到触发原因,但实际上是由于测试时,没有添加请求参数引起的,因此在遇到类似的异常时,需要注意请求本身是否存在违规的操作,缺失 header、参数等信息。
]]>在 uniapp 项目中使用 vue3 开发,在 setup script 标签中使用顶层 await 语句,导致页面空白,而控制台打印异常。
1 | 编译器: HBX 3.3 Beta |
这是因为 顶层 await 必须配合 async setup() 必须与 Suspense
组合使用,而当前版本的 uniapp 并不支持该组件。
在 setup script 中使用 nextTick 方法,在回调中使用 await 语句,而非在顶层使用。
1 | <script setup> |
需要对列表数据中某个字段验重。
通过自定义注解,借助 spring 参数校验框架,抽象通用逻辑,通过反射获取字段值进行比较。
1 | /** |
1 | /** |
1 | /** |
1 |
|
对于更新时的校验,为了防止字段值未修改,但因为在库中查询已存在,导致异常的情况,应该将 id 作为查询条件,筛选出所有不在 id 列表中的数据。
1 |
|
1 | <java.version>1.8</java.version> |
1 | const url = `{{ip}}:{{port}}/{{applitionName}}-{{env}}.yml`; |
1 | spring: |
当没有指定 spring.profiles.active=native
时,以读取 Git 仓库为配置,出现以下异常:
1 | *************************** |
注意:谨防在全局配置了环境变量,由于在系统环境变量中设置了公司常用的环境名,导致配置文件中的激活项被覆盖,而一直无法启动。
1 | spring: |
支持设置 uri 为本地路径,但需要该仓库存在,并且访问的配置(如:client-dev.yml)已被 git 托管。
1 | spring: |
由于在项目中使用明文配置密码,不太合适,较好的方式使用对其进行加密。
1 | <dependency> |
1 | jasypt: |
使用方式:
1 |
|
在程序中打印出加密后的字符串,并在配置文件中使用 ENC()
包裹,标识其为一串加密字符。
1 | ### 仅展示密码部分 |
到目前为止,配置中心的所有 api 都是可随意访问的,若需要对公网暴露,则需要添加登录鉴权更安全些。
添加 security 依赖,并简单配置:
1 | <dependency> |
1 | spring: |
1 | <dependency> |
1 | eureka: |
并在启动类上添加 @EnableEurekaClient
注解。
在登录授权时,需要从 header 中获取 token 进行鉴权,通常使用过滤器处理指定的访问请求,但在使用 @WebFilter
注解时,发现其路径匹配模式不生效,即 url、urlPatterns
属性配置后,拦截了所有请求。
@WebServlet,@WebFilter,@WebListener
注解,需要和 @ServletComponentScan
注解配套使用,表明其为 servlet 组件,而不是普通的 spring 组件,当使用 @Component
注解后,会优先被注册为 spring 组件,导致 servlet 扫描器失效。
1 |
|
可以通过 FilterRegistrationBean
装饰器,以编程方式手动注册过滤器。
由于在 bootstrap.yml
文件中配置 CI/CD 的环境变量,而本地开发时并没有相关环境,所以需要手动切换其属性,在提交代码时,需要关注该配置文件是否会与云端冲突,极其浪费精力。
因此需要能够屏蔽本地与云端配置差异的手段。
由于在测试过程中,发现使用 bootstrap-local.yml
的方式,无法成功切换环境,因此使用 spring 的指定外部配置文件的方式。通过在项目启动时,添加程序启动参数,手动指定配置文件所在路径 --spring.config.location=classpath:/local/
。
注意:local
是在 resources 下建立的目录,需要以 /
结尾表示其为目录。
当然,此处亦可精确指定加载哪个配置文件,但每个(放置在 resources 目录下的文件)都需要 classpath:/
开头,以表示其在类加载目录下,多个文件之间以 ,
分割。
出入参同时支持 XML 和 JSON 格式。
在接口请求映射器注解中,指定 consumes 的媒体类型,用于标记入参格式,指定 produces 的媒体类型,用于标记出参格式。
1 |
同时,需要在 pom 中添加 xml 处理依赖。
1 | <!-- 一般的由默认的 spring jackson 依赖控制版本号 --> |
使用 mybatis 的结果集自动映射,发生了实体属性类型与表字段类型不一致;但实际上,相关实体的属性与表结构完全匹配。
关于实体类,是使用代码生成读取表结构生成的,但使用了公共父类,其中抽取出了一些通用的字段(包括主键、创建时间等)。
1 |
|
不必计较父类中设置主键的合理性。
BaseEntity.java 是基于 mybatis-plus,因此所有字段添加了相应的注解。
实体中依赖 lombok 隐藏 getter、setter 的实现细节,并使用了 @Builder 辅助实体创建。
在代码中添加默认构造器(通过反射获取,与可见性无关)。
1 | lombok.allArgsConstructor.flagUsage = ERROR |
1 | private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException { |
由于 Optional 并不支持序列化,因此无法直接将其作为方法返回值、托管给 JetCache。必须对其进行拓展,添加自定义的编码器以及解码器。
依赖于 com.alicp.jetcache:jetcache-core:2.5.16。
只要将自定义的编码器注册为 bean 即可。
1 | jetcache: |
1 |
|
从源码中 SpringConfigProvider#parseValueEncoder
可以找到编码器的加载入口,当 valueEncoder 属性匹配到 “bean:”前缀的编码器时,则启用该 bean。
1 | // 1、ExternalCacheAutoInit#parseGeneralConfig |
在微服务项目中,需要请求第三方接口,通常情况下我们使用 spring 的 restTemplate。但接触到 feign 之后,这种将远程方法伪装成本地接口,屏蔽请求感知的方式,无疑更优雅。
但对于 oauth 认证,需要配置请求头,即设置 feign 的自定义配置。
@FeignClient
用于注册接口为 feign 客户端,在 spring cloud 项目中,需要在其 value(或 name)属性中指定对应的微服务名,url 置空(将自动匹配配置中西地址)。当需要调用任意的 http 请求时,只要给定 url 值即可。
注意 :此时,虽然不是请求微服务接口,仍然需要设置 name,填写任意(不冲突的)名称即可。
需要通过自定义配置属性来指定请求头,对应 @FeignClient
的 configuration 属性。
该属性指定一个自定义配置类,用于设置 feign 的任意配置。
1 | // @Configuration 托管给 spring 之后,会被注册为全局配置 |
1 |
|
1 | public class SomeTokenConfig { |
1 |
|
当响应信息实际返回的是 json 格式,但头信息描述类型是 Content-Type: text/xml;charset=utf-8 (或其他非 *application/json *类型)时,feign 的默认消息转换器无法解析,此时就需要重新使用自定义解码器。
在配置类中注册 Decoder bean。
1 |
|
feing 基于 http 请求,在其配置类中,可以实现任意 http 的相关配置,包括超时时间、重试次数等。
一般情况下,accessToken 存在过期时间,为了防止频繁请求授权,应该在过期之前进行缓存。
现需要实现同时共存两个版本的 SDK,其中存在限定名完全相同的类,但两个类的方法并不完全相同,导致 JVM 在加载时无法按预期的调用类方法。
由于 SDK 中依赖了大量的封装对象,无法使用简单的反射调用方法,因此自定义类加载器并不能解决。首先想到的是使用 maven 插件,在编译时修改指定的 SDK 的类名,maven-shade-plugin
并不能很好的解决,按照博客中的经验,可以将待处理的模块在一个空白模块中引入,并使用 shade 进行重命名,但是实际类引用的时候,依旧是原类名,只有在 maven 打包后,新名称将被替换值 class 文件中,这就导致了,如果有多个模块进行了调用 SDK,则需要处理多个模块,而且经常出现预期之外的问题,耗费精力。
最终是使用 jarjar 来解决的。
下载地址
1 | java -jar jarjar.jar process rule.txt old.jar new.jar |
其中,rule.txt 是任意文件名,其中指定了替换规则:
1 | rule com.old.package.** com.new.package.@1 |
或者使用 jarjar-maven-plugin(未验证)
]]>由于公司 SDK 没有正式上线,只能提供 jar,需要手动添加依赖,在使用 maven 打包的时候提示找不到符号。
将外部 jar 使用 maven 管理
1 | <dependency> |
使用 LocalDate 解析 yyyy-MM
格式的字符串时异常:
1 | Caused by: java.time.DateTimeException: Unable to obtain LocalDate from TemporalAccessor: {Year=2021, MonthOfYear=1},ISO of type java.time.format.Parsed |
字符串解析到 LocalDate 必须具体到日期,因此需要给年月的格式添加默认日期,
1 | public static void main(String[] args) { |
需要将工具类发布到公司的私有仓库中,为了避免每次手动在页面上操作,此处借助 maven 命令行和 idea 启动项实现一键发布。
1 | <!-- maven settings.xml --> |
1 | # 命令 |
此后就可以一键发布了。