参考文献

SpringBoot项目中正式环境中获取resources目录下报错

  • 错误信息
1
2
3
4
5
[] 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
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
at org.springframework.util.ResourceUtils.getFile(ResourceUtils.java:217)
at org.springframework.util.ResourceUtils.getFile(ResourceUtils.java:180)

  • 错误的获取方式

    1
    frontFile = ResourceUtils.getFile("classpath:fronts/Alibaba-PuHuiTi-Medium.ttf");
    • 该方式在调试阶段是没问题的,部署后会报上述错误
  • 正确的方式

    1
    InputStream resourceAsStream = PdfUtils.class.getResourceAsStream("/fronts/Alibaba-PuHuiTi-Medium.ttf")

SpringBootRedisTemplate存取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
    3
    Number 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
      @Bean
      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
        11
        public T deserialize(@Nullable 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项目使用WebSocketSafari浏览器中报错

  • https://www.v2ex.com/t/811568

  • 报错信息

    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 代理下无法工作,在开发者菜单中关掉之后一切正常.
  • 解决办法

    • 在 Safari 里的开发->试验性功能->NSURLSession WebSocket 取消勾选后问题消失.

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-Typeapplication/octet-stream,Web浏览器将识别该文件为二进制文件并直接下载
    • 参考如下两种方式,检查播放异常文件的Content-Type

      • Web浏览器工具:在播放异常的Web浏览器页面,打开Web浏览器的开发者工具(F12),切换到Network标签,找到播放的视频文件资源并单击其名称,然后单击其右侧的Headers标签,查看Content-Type值.

      img

      • OSS控制台:登录OSS控制台并进入对应的Bucket,找到目标Object资源,单击详情.在详情页面,单击设置HTTP头,查看Content-Type值.
    • 确认播放异常文件的文件格式与Content-Type值相匹配.比如MP4文件的Content-Typevideo/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
    @NoArgsConstructor
    @Data
    public class SearchStudiesResponse {


    @JsonProperty("SeriesInstanceUID")
    private List<String> SeriesInstanceUID;
    @JsonProperty("PatientName")
    private String PatientName;
    @JsonProperty("PatientID")
    private String PatientID;
    @JsonProperty("AccessionNumber")
    private String AccessionNumber;
    @JsonProperty("StudyDescription")
    private String StudyDescription;
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate StudyDate;
    @JsonProperty("BodyPartExamined")
    private String BodyPartExamined;
    @JsonProperty("InstitutionName")
    private String InstitutionName;
    @JsonProperty("StudyInstanceUID")
    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
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
protected void _addGetterMethod(Map<String, POJOPropertyBuilder> props,
AnnotatedMethod m, AnnotationIntrospector ai)
{
// Very first thing: skip if not returning any value
if (!m.hasReturnType()) {
return;
}

// any getter?
// @JsonAnyGetter?
if (Boolean.TRUE.equals(ai.hasAnyGetter(m))) {
if (_anyGetters == null) {
_anyGetters = new LinkedList<AnnotatedMember>();
}
_anyGetters.add(m);
return;
}
// @JsonValue?
if (Boolean.TRUE.equals(ai.hasAsValue(m))) {
if (_jsonValueAccessors == null) {
_jsonValueAccessors = new LinkedList<>();
}
_jsonValueAccessors.add(m);
return;
}
String implName; // from naming convention
boolean visible;

PropertyName pn = ai.findNameForSerialization(m);
boolean nameExplicit = (pn != null);

if (!nameExplicit) { // no explicit name; must consider implicit
implName = ai.findImplicitPropertyName(m);
if (implName == null) {
// okNameForRegularGetter或者okNameForIsGetter 最终调用到stdManglePropertyName或者legacyManglePropertyName
implName = BeanUtil.okNameForRegularGetter(m, m.getName(), _stdBeanNaming);
}
if (implName == null) { // if not, must skip
implName = BeanUtil.okNameForIsGetter(m, m.getName(), _stdBeanNaming);
if (implName == null) {
return;
}
visible = _visibilityChecker.isIsGetterVisible(m);
} else {
visible = _visibilityChecker.isGetterVisible(m);
}
} else { // explicit indication of inclusion, but may be empty
// we still need implicit name to link with other pieces
implName = ai.findImplicitPropertyName(m);
if (implName == null) {
implName = BeanUtil.okNameForGetter(m, _stdBeanNaming);
}
// if not regular getter name, use method name as is
if (implName == null) {
implName = m.getName();
}
if (pn.isEmpty()) {
// !!! TODO: use PropertyName for implicit names too
pn = _propNameFromSimple(implName);
nameExplicit = false;
}
visible = true;
}
boolean ignore = ai.hasIgnoreMarker(m);
_property(props, implName).addGetter(m, pn, nameExplicit, visible, ignore);
}
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
public static String okNameForRegularGetter(AnnotatedMethod am, String name,
boolean stdNaming)
{
if (name.startsWith("get")) {
/* 16-Feb-2009, tatu: To handle [JACKSON-53], need to block
* CGLib-provided method "getCallbacks". Not sure of exact
* safe criteria to get decent coverage without false matches;
* but for now let's assume there's no reason to use any
* such getter from CGLib.
* But let's try this approach...
*/
if ("getCallbacks".equals(name)) {
if (isCglibGetCallbacks(am)) {
return null;
}
} else if ("getMetaClass".equals(name)) {
// 30-Apr-2009, tatu: Need to suppress serialization of a cyclic reference
if (isGroovyMetaClassGetter(am)) {
return null;
}
}
return stdNaming
? stdManglePropertyName(name, 3)
: legacyManglePropertyName(name, 3);
}
return null;
}
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
/**
* Method called to figure out name of the property, given
* corresponding suggested name based on a method or field name.
*
* @param basename Name of accessor/mutator method, not including prefix
* ("get"/"is"/"set")
*/
protected static String legacyManglePropertyName(final String basename, final int offset)
{
final int end = basename.length();
if (end == offset) { // empty name, nope
return null;
}
// next check: is the first character upper case? If not, return as is
char c = basename.charAt(offset);
// 首字母小写
char d = Character.toLowerCase(c);

if (c == d) {
return basename.substring(offset);
}
// otherwise, lower case initial chars. Common case first, just one char
StringBuilder sb = new StringBuilder(end - offset);
sb.append(d);
int i = offset+1;
for (; i < end; ++i) {
c = basename.charAt(i);
d = Character.toLowerCase(c);
if (c == d) {
sb.append(basename, i, end);
break;
}
sb.append(d);
}
return sb.toString();
}

解决方法

  • 重写Get方法,并加上@JsonProperty("StudyDate")信息
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
@NoArgsConstructor
@Data
public class SearchStudiesResponse {


@JsonProperty("SeriesInstanceUID")
private List<String> SeriesInstanceUID;
@JsonProperty("PatientName")
private String PatientName;
@JsonProperty("PatientID")
private String PatientID;
@JsonProperty("AccessionNumber")
private String AccessionNumber;
@JsonProperty("StudyDescription")
private String StudyDescription;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate StudyDate;
@JsonProperty("BodyPartExamined")
private String BodyPartExamined;
@JsonProperty("InstitutionName")
private String InstitutionName;
@JsonProperty("StudyInstanceUID")
private String StudyInstanceUID;

@JsonProperty("StudyDate")
public LocalDate getStudyDate() {
return StudyDate;
}
}

总结

  1. Jackson默认序列化时只会处理有对应的get/set方法的字段,私有字段不会参与序列化。
  2. @JsonProperty注解可以标注在私有属性上,表示在序列化时可见,并且可以指定序列化的名称。
  3. 如果@JsonProperty注解标注在get/set方法上,则不会出现问题。
  4. 在使用Lombok的@Data注解时,只能将注解标注在属性上,这可能导致问题。
  5. 解决办法是手动编写get/set方法,并将注解标注在方法上。

使用Redis缓存类的时候报错DefaultSerializer requires a Serializable payload but received an object of type[]

问题原因

  • 被缓存的类未实现Serializable接口

处理方法

  • 被缓存的类实现Serializable接口