SpringBoot-遇到的问题
参考文献
SpringBoot
项目中正式环境中获取resources
目录下报错
- 错误信息
1 | [] 2023-07-04 10:20:51.624 [http-nio-3345-exec-8] ERROR o.a.c.c.C.[.[localhost].[/].[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: java.io.FileNotFoundException: class path resource [fronts/Alibaba-PuHuiTi-Medium.ttf] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/usr/local/skyward/ct-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/fronts/Alibaba-PuHuiTi-Medium.ttf] with root cause |
-
错误的获取方式
1
frontFile = ResourceUtils.getFile("classpath:fronts/Alibaba-PuHuiTi-Medium.ttf");
- 该方式在调试阶段是没问题的,部署后会报上述错误
-
正确的方式
1
InputStream resourceAsStream = PdfUtils.class.getResourceAsStream("/fronts/Alibaba-PuHuiTi-Medium.ttf")
SpringBoot
之RedisTemplate
存取Long
类型数据自动变Integer
问题
-
错误信息
1
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Long (java.lang.Integer and java.lang.Long are in module java.base of loader 'bootstrap')
-
错误的获取方式
- 使用强转的方式来获取
1
Long count = (Long) redisUtils.get(key);
-
正确的获取方式
1
2
3Number count = (Number) redisUtils.get(key);
System.out.println("需要int时"+n1.intValue());
System.out.println("需要long时"+n1.longValue()); -
原因
-
Redis的序列化配置为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(jackson2JsonRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}-
实现反序列化方法,反序列化类设置的为
Jackson2JsonRedisSerializer
.注意!这里统一将结果反序列化为Object类型,所以这里便是问题的根源所在,对于数值类型,取出后统一转为Object,导致泛型类型丢失,数值自动转为了Integer类型.1
2
3
4
5
6
7
8
9
10
11public T deserialize(byte[] bytes) throws SerializationException {
if (SerializationUtils.isEmpty(bytes)) {
return null;
}
try {
return (T) this.objectMapper.readValue(bytes, 0, bytes.length, javaType);
} catch (Exception ex) {
throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
}
}
-
-
SpringBoot
项目使用WebSocket
在Safari
浏览器中报错
-
报错信息
1
WebSocket connection to 'wss://xxxxx/ws/' failed: The operation couldn’t be completed. (kNWErrorDomainPOSIX error 57 - Socket is not connected
-
原因
- Monterey 的 Safari 开启了
NSURLSession WebSocket
这个实验性特性,会导致 WebSocket 在 HTTPS 代理下无法工作,在开发者菜单中关掉之后一切正常.
- Monterey 的 Safari 开启了
-
解决办法
- 在 Safari 里的开发->试验性功能->
NSURLSession WebSocket
取消勾选后问题消失.
- 在 Safari 里的开发->试验性功能->
SpringBoot
项目使用OSS
上传视频文件,通过外链访问无法正常播放
问题描述
- 上传到阿里云对象存储OSS的视频在线播放时出现异常,一般存在以下两种情况:
- 情况一:视频无法在线播放.
- 情况二:视频在线播放时只有声音没有画面.
问题原因
- 针对不同的情况,问题原因不同,具体如下:
- 情况一:视频无法在线播放
- 问题原因:浏览器无法正常识别该视频文件的类型.
- 情况二:视频在线播放时只有声音没有画面
- 问题原因:OSS作为存储服务,不对音视频文件进行任何处理,但Web浏览器支持解码常见的音视频文件.通过Web浏览器访问OSS中的音视频资源时,Web浏览器对该音视频资源进行解码并播放.如果该视频文件为MPEG4或HEVC等格式(H.265编码),常见的Web浏览器暂不兼容该编码的视频文件,导致浏览器只解码了音频,没有解码视频.
解决方案
- 对于上述两种情况,其解决方案不同,请根据现场实际情况选择对应的解决方案.
情况一:无法在线播放视频文件
- Web浏览器通过文件对应的
Content-Type
来识别文件类型,在OSS的控制台或者使用OSS的SDK上传文件时,通常会自动匹配常见文件的Content-Type
.但是使用API上传时,需要用户自定义文件的Content-Type
,否则OSS会默认设置文件的Content-Type
为application/octet-stream
,Web浏览器将识别该文件为二进制文件并直接下载-
参考如下两种方式,检查播放异常文件的
Content-Type
值- Web浏览器工具:在播放异常的Web浏览器页面,打开Web浏览器的开发者工具(F12),切换到Network标签,找到播放的视频文件资源并单击其名称,然后单击其右侧的Headers标签,查看Content-Type值.
- OSS控制台:登录OSS控制台并进入对应的Bucket,找到目标Object资源,单击详情.在详情页面,单击设置HTTP头,查看Content-Type值.
-
确认播放异常文件的文件格式与
Content-Type
值相匹配.比如MP4文件的Content-Type
为video/mp4
.如果不匹配,请参考上述步骤中的OSS控制台方式,修改该文件的Content-Type
为正确的值. -
如果播放异常文件的
Content-Type
正确,但是在Web浏览器中播放时仅有音频,没有视频,则可能是由于该视频文件需要使用特殊的解码器才可以正确播放,而浏览器不支持此解码器.可参阅情况二:在线播放时只有声音没有画面进行解决.
-
情况二:在线播放时只有声音没有画面
-
查看该视频文件的编码格式,如果该视频为MPEG4或HEVC等H.265编码,请根据实际情况选择如下任一解决方案.
-
说明:您可以通过第三方工具,如MediaInfo,来确认视频文件的编码格式.
- 将该视频文件转码为H.264编码后重新上传至OSS,详情请参见添加转码配置.
- 将该视频文件下载到本地,通过第三方播放器(OBS推流工具或者VLC播放器)播放.
- 在页面中嵌入支持该视频的播放器插件,通过该插件播放视频.
-
适用于
- 对象存储 OSS
使用@JsonProperty
注解会多出一个字段
com.fasterxml.jackson.annotation.JsonProperty
现象
-
兼容老代码,将下面的
Bean
中的字段命名格式定义为首字母大写的格式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SearchStudiesResponse {
private List<String> SeriesInstanceUID;
private String PatientName;
private String PatientID;
private String AccessionNumber;
private String StudyDescription;
private LocalDate StudyDate;
private String BodyPartExamined;
private String InstitutionName;
private String StudyInstanceUID;
} -
在接口返回时会多一个
studyDate
字段1
2
3
4
5
6
7
8
9
10
11
12
13{
"StudyDate": "2023-04-01",
"studyDate": "2023-04-01",
"SeriesInstanceUID": [
"xxxx"
],
"PatientName": "ZHA SI",
"PatientID": "CT458082",
"AccessionNumber": "xxxx",
"StudyDescription": "",
"BodyPartExamined": "",
"InstitutionName": "xxxx"
}
分析
以下是引用的原文: jackson2对pojo类型序列化的处理。
Jackson2在初始化序列器时,对pojo类型对象会收集其属性信息,属性包括成员变量及方法,然后属性名称和处理过后的方法名称做为key保存到一个LinkedHashMap中。收集过程中会调用com.fasterxml.jackson.databind.util.BeanUtil中的legacyManglePropertyName方法用来处理方法名称,它会将get/set方法前缀,即get或set去掉,并将其后面的连续大写字符转换成小写字符返回。例如: getNEWString会转变成newstring返回。你的属性名称如果有这样的"nSmallSellCount",lombok自动生成的get方法就会是这样的"getNSmallSellCount",处理过后就是这样的"nsmallSellCount",这与属性nSmallSellCount并不冲突,可以同时存在于HashMap中。收集完属性信息后,下面的步骤中会删除掉非可见的属性,一般指的是私有成员变量,这时,名称为"nSmallSellCount"的成员变量属性会被删除掉,这样的序列化结果是不会有问题的,但,如果加了@JsonProperty注释,Jackson2会认为这个属性是可见的,不必会删除,这时这两个表示同一个值得属性就会被一同序列化。
用jackson的@JsonProperty注解属性名,会多出一个字段:用jackson的@JsonProperty注解属性名,会多出一个字段
1 | protected void _addGetterMethod(Map<String, POJOPropertyBuilder> props, |
1 | public static String okNameForRegularGetter(AnnotatedMethod am, String name, |
1 | /** |
解决方法
- 重写
Get
方法,并加上@JsonProperty("StudyDate")
信息
1 |
|
总结
- Jackson默认序列化时只会处理有对应的get/set方法的字段,私有字段不会参与序列化。
- @JsonProperty注解可以标注在私有属性上,表示在序列化时可见,并且可以指定序列化的名称。
- 如果@JsonProperty注解标注在get/set方法上,则不会出现问题。
- 在使用Lombok的@Data注解时,只能将注解标注在属性上,这可能导致问题。
- 解决办法是手动编写get/set方法,并将注解标注在方法上。
使用Redis
缓存类的时候报错DefaultSerializer requires a Serializable payload but received an object of type[]
问题原因
- 被缓存的类未实现
Serializable
接口
处理方法
- 被缓存的类实现
Serializable
接口
项目集成Spring Security,使用@Async
标注方法,在该方法内获取SecurityContextHolder.getContext()
时,获取失败
现象
- 被标注了
@Async
的方法无法获取SecurityContextHolder.getContext()
分析
-
由于使用
@Async
执行到方法时会切换线程,而``SecurityContextHolder.getContext()默认使用的策略为
MODE_THREADLOCAL`,只会在同一个线程中才能获取到上下文.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// org.springframework.security.core.context.SecurityContextHolder
// spring-security-core-5.7.3
private static void initializeStrategy() {
if (MODE_PRE_INITIALIZED.equals(strategyName)) {
Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
+ ", setContextHolderStrategy must be called with the fully constructed strategy");
return;
}
if (!StringUtils.hasText(strategyName)) {
// Set default 默认策略
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
return;
}
// Try to load a custom strategy
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
解决办法
-
方法一: 修改
SecurityContextHolder
的策略为MODE_INHERITABLETHREADLOCAL
1
2
3
4
5
6
public InitializingBean contextStrategy() {
return () -> SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
);
}-
风险:
-
线程池复用可能导致不同任务的上下文交叉污染
-
需在异步任务结束时清理上下文
1
2
3
4
5
6
7
8
public void asyncMethod() {
try {
// 执行业务逻辑
} finally {
SecurityContextHolder.clearContext();
}
}
-
-
-
方法二: 配置支持上下文传播的异步执行器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* 异步线程池配置类
*
* @author HoleLin
*/
public class AsyncConfig implements AsyncConfigurer {
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(200);
executor.setKeepAliveSeconds(5 * 60);
executor.setQueueCapacity(1000);
// 自定义实现拒绝策略
executor.setRejectedExecutionHandler((Runnable runnable, ThreadPoolExecutor exe) -> log.error("当前任务线程池队列已满."));
// 线程名称前缀
executor.setThreadNamePrefix("Async-");
executor.initialize();
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
}-
使用
DelegatingSecurityContextAsyncTaskExecutor
会自动清理上下文1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// org.springframework.security.concurrent.DelegatingSecurityContextRunnable
public void run() {
this.originalSecurityContext = SecurityContextHolder.getContext();
try {
SecurityContextHolder.setContext(this.delegateSecurityContext);
this.delegate.run();
}
finally {
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
if (emptyContext.equals(this.originalSecurityContext)) {
SecurityContextHolder.clearContext();
}
else {
SecurityContextHolder.setContext(this.originalSecurityContext);
}
this.originalSecurityContext = null;
}
}
-