一、Java
1. Excel文件导出中文名称时被转义
1 | String fileName = URLEncode.encode(name, "UTF-8"); |
文件的下载在响应之前要设置以 附件(attachment) 的形式,否则点击下载时会在浏览器打开。
Spring MVC 默认的上传文件大小(max-request-size)最大为10M,可以在配置文件中修改。
二、Spring
1. Spring Security 5.x 跨线程获取用户信息
使用 Spring Security 时,如果采用异步方法获取用户信息是获取不到的,它采用 ThreadLocal 存储,这样的话有些请求无法顺利执行,可以在 Spring Boot 启动类主方法添加:
1 | public static void main(String[] args) { |
而使用 Security 6.x+ 版本时,直接使用 SecurityContextHolder.setStrategyName(SecurityContextHolder。MODE_INHERITABLETHREADLOCAL);
开启用户信息在异步线程或者子线程中获取会导致用户无法正常通过验证。所以如果需要在子线程中获取用户信息,可以考虑在主线程中把 SecurityContent 传递给子线程或者异步线程。
2. @RestControllerAdvice + swagger 导致的问题
如果在项目中实现 ResponseBodyAdvice
接口统一封装 controller 返回的接口,访问 swagger 地址时会出现弹窗 Unable to infer base url. This is common when using dynamic servlet registration or when the API is behind an API Gateway
的问题。如果是使用的 Knife4j,则访问时可能会显示获取文档错误的信息。
主要是由于被拦截了,因为 @RestControllerAdvice 的工作机制是在 controller 层的结果返回时自动对其包装(前提是 supports 方法返回 true),在默认的情况下,文档也被修改了,因此无法正常获取。
第一种方式最简单,需要在实现 ResponseBodyAdvice
的类注解上加入要扫描的包名(一般是 controller),例如 @RestControllerAdvice(basePackages = "xxx.xx.controller")
来限定需要对返回结果进行封装的范围,这样的话就不会拦截 swagger 相关资源的地址,访问就正常了。
但是随着项目的不同会有许多不同的情况,包名可能没有办法统一格式,好在该注解有另一个属性:annotation,我们可以自定义一个注解,然后所有的 controller 都使用自定义的注解,再使用 @RestControllerAdvice(annotation = 自定义注解.class)
也没有问题。
另外一种方式就是在 supports()
中修改,遇到包含 swagger 的就放行。
1 |
|
也可以下面这样修改(浪子没有测试,具体见:https://juejin.cn/post/6921700441038258189):
1 |
|
3. 自定义 Jackson 转换器对 swagger 的影响
项目中我们可能使用了 Jackson 作为 json 工具,那么很有可能自定义了转换工具,如果把转换工具放在第一个,那么也会对 swagger 造成影响,直接就返回了一串类似 token 的字符串(web 控制台可以查看。不过具体返回什么,需要自己 Debug 看看什么地方的问题)
1 |
|
上面使用 swagger 的集成多多少少会造成一些问题,而且 swagger 对代码的侵入较强,浪子不推荐使用。如果公司项目还是推荐开发人员自行维护接口,例如使用 Apifox、ApiPost、Postman、Yapi 等,这些工具中有的提供了私有化部署,团队维护最合适不过。
4. 跨域
1 | // 注解用在 Controller 类中,该类所有方法允许其它域中进行访问 |
1 |
|
1 |
|
5. 在 Filter 中注入 Spring Bean
在 Spring Web 中,执行顺序是 context-param–>listener–>filter–>servlet,可以看到 servlet 是在最后的,所以在 Filter 中注入 Bean 的时候就会报 NPE,如果是 SpringBoot 项目,可以在 Filter 的实现类上使用 @Order 注解延迟加载,让 Bean 先行加载,这样就可以在 Filter 中注入了。
6. Spring AOP 和 Spring Security 6.X
1 | Cannot invoke "org.apache.commons.logging.Log.isDebugEnabled()" because "this.logger" is null |
背景:多模块项目,AOP 依赖在 common
模块,Security 在 system
模块,system 依赖于 common。使用 @Aspect
定义了切面和日志,但是定义切入点的时候是 execution(* com.xxx..*.*(..))
。com.xxx
项目是顶级包。
而 Security 中的 OncePerRequestFilter
继承的 GenericFilterBean#init()
中有如下代码:
1 | if (logger.isDebugEnabled()) { |
在进行切入的时候,把这里的 logger
给覆盖了,导致一直拿不到值,项目启动就抛出了 NPE。
修改很简单,打印日志一般抓 controller
层,只需要把切入点定义改为:execution(* com.xxx.controller..*.*(..))
。更加推荐的是把 AOP 依赖直接移入 system
中,相关的日志类也放到该模块,这里出现错误是因为之前的划分原本就不是很合理。
7. @PathPathVariable 使用时建议指定参数名称,例如 @PathPathVariable(“id”)
8. Spring MVC PathPattern、AntPathMatcher
PathPattern
是 Spring 5.0 提供的,在 Spring 6.x 中有了改进并被设置默认值。本项目使用 PathPattern
,AntPathMatcher
是旧版的路径匹配器。如果是新项目的话,推荐使用新的的 PathPattern
。如果项目中没有配置 spring.mvc.pathmatch.matching-strategy
,默认使用的也是 PathPatternParser
。
新版本中, @RequestMapping(“/xxx”) 会自动在指定的路径后加入 /.
9. 使用构造注入?
以下内容摘自 Spring 官网:
Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory
dependencies and setter methods or configuration methods for optional dependencies. Note that use of the @Autowired
annotation on a setter method can be used to make the property be a required dependency; however, constructor
injection
with programmatic validation of arguments is preferable.The Spring team generally advocates constructor injection, as it lets you implement application components as
immutable objects and ensures that required dependencies are not . Furthermore, constructor-injected components are
always returned to the client (calling) code in a fully initialized state. As a side note, a large number of
constructor
arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored
to
better address proper separation of concerns.nullSetter injection should primarily only be used for optional dependencies that can be assigned reasonable default
values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One
benefit of setter injection is that setter methods make objects of that class amenable to reconfiguration or
re-injection later. Management through JMX MBeans is therefore a compelling use case for setter injection.Use the DI style that makes the most sense for a particular class. Sometimes, when dealing with third-party classes
for which you do not have the source, the choice is made for you. For example, if a third-party class does not expose
any setter methods, then constructor injection may be the only available form of DI.
机翻一下:
由于您可以混合基于构造函数和基于 setter 的 DI,因此这是一个很好的经验法则 将构造函数用于必需依赖项和 setter 方法或配置方法用于可选依赖项。请注意,在 setter 方法上使用 @Autowired 注释可用于使属性成为必需的依赖项; 但是,最好使用参数的编程验证进行构造函数注入。
Spring 团队通常提倡构造函数注入,因为它可以让你实现应用程序组件作为不可变对象,并确保所需的依赖项不是 null.此外,构造函数注入的组件始终返回给客户端(调用)处于完全初始化状态的代码。顺便说一句,大量的构造函数 arguments 是一种不好的代码气味,这意味着该类可能有太多责任,并应进行重构,以更好地解决适当的关注点分离问题。
Setter 注入应主要仅用于可选的依赖项,这些依赖项可以 在类中分配合理的默认值。否则,非 null 检查必须在代码使用依赖项的任何地方执行。入孵注入的一个好处是 setter 方法使该类对象适合重新配置或重新注入 后。因此,通过 JMX MBean 进行管理是一个引人注目的二传手注入的用例。
使用对特定类最有意义的 DI 样式。有时,在处理时 对于您没有源代码的第三方类,您可以进行选择。例如,如果第三方类不公开任何 setter 方法,则构造函数注射可能是唯一可用的 DI 形式。
1 | import org.springframework.beans.factory.annotation.Autowired; |
可以使用 lombok 的注解来添加构造方法,不必手动添加。
10. SpringBoot 3.x
Jakarta EE 9是一个新的顶级包,取代了 Java EE 中的 javax 顶级包。例如,JakartaEE8 中的 Servlet 规范使用 javax.servlet
包,但在 EE9 中已更改为 jakarta.servlet
。
一般来说,不可能在同一个项目中混合使用 Java EE 和 Jakarta EE API。您需要确保您自己的代码以及所有第三方库都使用 jakarta.*
包导入。大多数维护良好的库都在生成 Jakarta EE 9 兼容的变体。例如 Hibernate、Thymeleaf、Tomcat、Jetty 和 Undertow 都已经这样做了。
以上机翻自 Spring 官网。
三、MyBatis/MyBatis Plus(MP)
缓存
一级缓存默认开启,基于 SqlSessionFactory,整个会话共享。
二级缓存也可以说是本地缓存(一个 Map 缓存),存储在项目应用自身中。在 mapper.xml 中使用 <cache/>
标签即可开启。
对于具有关联关系(多表连接查询)的缓存,在从表的 mapper.xml 中使用 <cache-ref namespace = "" />
标签,namespace 指向主表/相关联的表。二级缓存也解决了缓存穿透的问题,如果请求了一个不存在的数据,那么 MyBatis 也会进行缓存,值为 null。
自定义缓存是在二级缓存的基础上实现的,Mybatis 默认使用 org.apache.ibatis.cache.impl.PerpetualCache
类。我们可以对内部的方法进行覆写,从而实现自定义缓存。
- 实现 Mybatis 的
Cache
接口; - 在 mapper.xml 中使用
<cache type="自定义缓存类全限定名"/>
标签;
Result Maps collection does not contain value for ……
在 SQL 文件中使用了 resultType="java.util.map"
,并且不止一处使用,那么凡是使用 resultType
或者 resultMap
这种属性的标签,不能有属性指向错误,否则就会报错: “Result Maps collection does not contain value for ……”
Parameter index out of range (2 > number of parameters, which is 1).
不能在 mybatis 的 sql.xml
中的 sql 标签(<select>、<update>、<delete>等
)中注释 sql 语句,如果注释的语句中 带有参数,那么就会报这个错误。如果有必须注释的语句,把相关语句复制一份放在 mybatis 标签外面使用<!-- 注释 sql 内容 -->
注释。如果可以,还是把 SQL 语句存储在外部的 txt 等文本文件里。
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)
有这个错误的话就是 mapper 接口和对应的 xml 文件没有绑定,或者绑定了,但是 xml 文件不是在 resources/mapper
路径下。需要在项目的 pom.xml
中添加以下内容:
1 | <!-- 把 xml 文件一起打包 --> |
1. $ And #
这个我想很多人都知道使用哪个比较好了,说说问题。具体语句省略,请注意循环内的条件。
1 | select * from user u |
上面的语句在循环中使用了 ${}
,我把它修改成了 #{}
,但是输入的筛选的条件是不会生效的,继而没有返回数据。
之后看了看它传的参数,发现接口接收的字符串,然后在业务层转为字符串集合,并且为每个元素加入单引号后传入 SQL 的。
1 | private static List<String> getParamList(String propertyType) { |
于是猜想可能就是这个原因导致查询条件不生效,于是把这段代码改成了下面的样子:
1 | private static List<String> getParamlist(String str) { |
这段代码没有给分割后的字符加入英文单引号,map 方法对每一个元素都去掉前后的空格(请结合自身业务场景确定参数是否可包含空格,浪子这里不需要),然后再次测试该接口,perfect!it is working now!
之后使用修改后的字符转集合方法代码再次使用 ${}
去测试,发现 SQL 语句查询又不生效了。。。
于是浪子明白了:使用 ${}
时,参数为 List 类型需要我们为每个元素手动加入单引号,使用单引号包裹才会生效;而使用 #{}
时,List 参数类型则不需要我们手动加入英文单引号,直接传入 List 即可。
2. 接口传参报错
错误信息:
1 | No primary or default constructor found for interface java.util.List] |
后端使用 List<String>
或者数组接参数收时,前端传入数组接收不到;但是可以传字符串,每个元素可以使用英文 ,
分割。
3. MP插入或更新null值
当使用 Mybatis Plus 时,更新数据为 null 值时,即使数据库的字段设置为可以为 null,但是更新或插入时还是数据更新失败。这个就涉及到字段验证策略了。MP 官网也给出了 解决办法
4. mapper.xml
在 mapper.xml
中编写 SQL 语句时有部分字符需要转义才可以被框架正确的解析:
转义字符 | 含义 | 说明 |
---|---|---|
> | > | greater than |
< | < | less than |
& | & |
也可以使用 <![CDATA[]]>
来声明语句,被这个标记所包含的内容将表示为纯文本。
MP 就是垃圾!建议放弃,使用 Jimmer!!!
5. MP 的动态数据源插件
如果一个方法中开启了 Spring 的事务,那么将不会切换数据源,需要使用 MP 提供的 @DSTransactional
。如果在同一个方法中调用多个数据源的业务,可以采用以下方式:
1 |
|