Spring的那些坑

CodeingBoy 2月 16, 2018

这篇文章列举了我在学习、使用 Spring 框架(包括但不限于 Spring Security, Spring MVC, Spring Data 等派生框架)过程中产生的疑问及问题。

Spring Security 的配置上下文问题

最近按照《Spring 实战(第四版)》进行一个新项目的配置,前面的几步都很轻松,直到配置 Spring Security 的时候就出了问题。

日志提示:

运用排除法可以判断是 Spring Security 的配置出了问题。跑到 Tomcat 的 localhost log 一看,啥都没有,空空如也:

后面才发现是消息级别设置错误了,点击右上角的 Warning 设置成 All 就可以看到如下信息:

至此,可以确定是 Spring Security 的问题无疑。但是,配置的步骤都和书上的一致,按照书上的说法,在 WebSecurityConfigurerAdapter 的子类中标注了 @EnableWebSecurity,就应该能够把 springSecurityFilterChain 这一系列的 filter 配置妥当。但是从日志看来,并没有。

经过查找和搜索,才发现上述 WebSecurityConfigurerAdapter 的子类 SecurityConfig,应该列在 RootConfigClasses 里面,而不是 ServletConfigClasses 里面。更换后问题解决。

为什么书中的例子没有使用这种办法也可以运行妥当呢?秘密就在于它的 RootConfig 类,书中的 RootConfig 类使用了如下的注解:

这会使得,即使是在 web 包中的 Config 类,也会被 RootConfig 组件扫描到,最终可以进行初始化。

按照 Spring 官方关于 Spring Security 的文档,其实我们还可以在我们的 AbstractSecurityWebApplicationInitializer 子类的构造器中传入配置类,代码如下,但是我尝试后发生了以下错误:

提示创建了多次 ContextLoaderListener 的 context。

参考:https://stackoverflow.com/questions/23572516/no-bean-named-springsecurityfilterchain-is-defined-error-with-javaconfig?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa、https://spring.io/blog/2013/07/03/spring-security-java-config-preview-web-security/

Spring Security 莫名其妙的密码验证错误

初次学习使用 Spring Security 的时候感觉 Spring Security 真是挺方便的,但是在配置好之后却出现以下问题:

按照 Spring Security 的模式配置好了数据库,其中账户数据表我使用了自己的 schema,而其它两个表则是按照 Spring 默认的数据库模式。对应的 Java Config 代码如下:

配置好了数据库,也填入了对应的信息,正打算登录试试看,却发现一直提示密码错误。然而密码却是正确的。

检查了输入的密码和数据库中对应的密码一致,百思不得其解。难道是我的 login 拦截器没配置好吗?尝试修正后发现也不是这里的问题。

最后对相关方法进行断点调试,最初的时候 hash 是一致的,但是后面竟然又不一致了?最终发现是另外一个数据表——authorities 的问题,由于我没有在该表中填入用户的权限信息,Spring 在这个表中查找不到该用户的权限信息,就会出现错误。

解决办法很简单,在用户注册的时候也往 authorities 表中填写用户的权限信息即可。

Repository 的 Proxy 问题

在编写 Hibernate 的 Repository 的时候出了个这样的问题:

首先我要说明一下我编写 Repository 的类结构,我并没有使用 Spring 主推的接口 + 实现方法,而是按照如下结构进行编写:
BaseRepository (abstract class with implementation)- extends -> ConcreteRepository (concrete class with implementation)

在我看来,这样的结构适合我在 BaseRepository 中编写一些公共实现代码,并且能够在 ConcreteRepository 中编写针对各个 Model 的更具体的代码。

在 Controller 中使用时,我直接就使用了实现类进行自动装配:

当然这个和使用接口并没有什么冲突,我可以为 ConcreteRepository 创建一个接口,然后再在 Controller 中使用接口来装配。但是 ConcreteRepository 数量繁多,并且是从遗留代码中稍加改造而来的,我不太愿意花太多精力在这个事情上面。

使用的时候就出问题了:

怪了,哪来的代理啊……

搜索了一下,发现这个问题是因为 Spring 需要对 Repository 创建代理以完成诸如事务处理等工作,但是由于我在上面使用了具体类,因此创建代理后的实际类型就和具体类的类型不符了。

一个简单解决办法当然是为每一个 ConcreteRepository 创建一个接口,这样就不会出现这种问题了。另外一个解决办法是换用 CGLIB 的代理方法(虽然不是很清楚这个代理和 JDK 动态代理具体不一样的地方)。

换用 CGLIB 的方法很简单,只需要在 @Configuration 类中添加如下注解即可:

当然,你还需要引入 cglib 的依赖。

但是加上去后还是老样子啊,按照搜索到的其它方法,尝试添加 @EnableTransactionManagement(proxyTargetClass = true) 也无果。

最后根据某个帖子的解决办法,发现其实问题在于 BeanPostTranslator 上,需要在 Bean 声明中将该项打开才行:

设置好后就没问题了。

Hibernate Repository 的事务 Session 问题

我学习 Spring 使用的书籍是《Spring 实战》,按照书里面的步骤,我配置好了 DataSource,SessionFactory 和 Repository。但是却遇到了以下问题:

查了一下,需要添加一个事务处理器:

整合步骤和上述的代码可以从 http://www.baeldung.com/hibernate-5-spring 中得到。

再看了一下,需要在 Repository 中添加 @Transactional 注解。添加后又出现上面的 Proxy 问题,添加 @EnableTransactionManagement(proxyTargetClass = true) 后解决。

完成上述步骤后,还是一样的问题。干脆进行调试,从以下代码开始跟踪:

一直跟踪到 SpringSessionContextcurrentSession() 方法,跟着跟着就发现不太对劲了——怎么这个 context 的 transactionManager 的属性是 null?明明已经配置了事务处理器了啊?

最后发现是 BaseRepository 中没有加上 @Transactional 注解,加上后就 OK 了。由于该注解可以被继承,因此诸多的 ConcreteRepository 也不需要加这个注解了。

另外在跟踪该方法时,可能会以为和 jtaPlatform 变量有关(显示的是 NoJtaPlatform),但实际和这个没关系,不要尝试修改 hibernate.transaction.jta.platform。另外也不要修改 hibernate.current_session_context_class,保持默认的 SpringSessionContext 即可。

参考资料:https://stackoverflow.com/questions/40084173/spring-hibernate-migration-currentsession-method-throwing-exception、http://blog.csdn.net/qq_18757713/article/details/51250101

Hibernate 检索数据输出 JSON 时导致的延迟加载(Lazy Initialize) 问题

搞定上述问题后可以正常检索了。由于我要开发一个 REST API,因此需要使用 Spring 的消息转换器(Message Converter)将检索的对象转换为 JSON 字符串发送给客户端。

然后就出现以下错误:

用过 Hibernate 基本都知道这是怎么回事:Hibernate 默认会对对象属性进行懒加载,它使用代理模式在查询实际发生的时候才实际查询属性。但是一旦关联的 Session 被关闭,对象就变为 detach 状态,无法进行懒加载属性的查询。

解决这个问题有好几种办法:

  • 放弃懒加载,全部属性一次性加载完
  • 使用 Spring 的 OpenSessionInView 模式,在一个请求中保持一个 Session
  • 使用 DTO——创建一个类,将检索出的对象的信息填充到这个 DTO 类,起到接力的作用
  • 使用 @JsonIgnore 注解让 Jackson 忽略某些属性
  • 使用 jackson-datatype-hibernate 库:https://github.com/FasterXML/jackson-datatype-hibernate

列举一下缺点:

  • 放弃懒加载的话,将会对所有查询生效,因此会影响数据库查询的效率(API 只是数据库查询的一个应用而已)
  • OpenSessionInView 在 StackOverflow 上被人称作反模式?
  • 创建 DTO 所需要的类和代码较大
  • 忽略懒加载集合只是治标不治本的做法

那就使用 jackson-datatype-hibernate 库吧!但是一番配置后发现这个库只支持基本属性,不支持集合,而且对 Hibernate 5.2 以上的版本不适用。(配置方法:https://stackoverflow.com/questions/21708339/avoid-jackson-serialization-on-non-fetched-lazy-objects/21760361#21760361、https://stackoverflow.com/questions/38273640/spring-hibernate-jackson-hibernate5module)

使用 DTO

既然无法使用 jackson-datatype-hibernate,那就只能退而求其次,使用 DTO 作为中继来传递数据。可以使用 ModelMapper 库来简化 Entity 和 DTO 之间的映射关联代码,详细见:http://www.baeldung.com/entity-to-and-from-dto-for-a-java-spring-application

按照这样映射之后,还是出现了一样的错误。看样子,得要搞明白两件事情:

  1. 什么时候 Session 会自动关闭?
  2. 应该怎样在 Session 打开的时候读取完毕所有数据?

Session 与事务管理

一番查阅之后才发现,Spring 的 @Transactional 规定了事务边界,Spring 将会在事务结束的时候关闭 Session。也就是说,@Transactional 不仅要加在 Repository 上,还需要加到 Controller 里面,以表明事务是从 Controller 方法开始的,这样在从 Repository 返回 Contoller 的处理方法时,不会将 Session 关闭掉。

需要注意的是,需要在对应的 Context 的配置类上加上 @EnableTransactionManagement。由于 Controller 是在 WebApplicationContext 上注册的,因此要将上面的注解加在 WebApplicationContext 的配置类上。如果加到了错误的配置类上将不起作用。

关于 Spring 的事务管理,这里有一篇文章讲的不错:https://www.ibm.com/developerworks/cn/education/opensource/os-cn-spring-trans/index.html

使 Hibernate 强制读取懒加载属性/集合

如果阅读过 Hibernate 相关资料的话,应该会知道 Hibernate.initialize可以强制读取懒加载属性或集合。但是给类的每个属性都加上这些手工加载代码显然不是什么好事。所以要使用反射对类的所有成员调用一次Hibernate.initialize 来确保成员都被加载了,这样即使 Session 关闭了也不会抛出异常。

这里有一份使用反射对类成员进行加载的代码,可以直接复制过来使用。但是对于级联的集合,还需要递归加载才行,因此我修改了一下这份代码:

这样就 OK 了。然后对要强制加载的属性或集合加上 @LazyCollection 注解即可。

使用切面简化 Controller 方法

使用上面的代码已经可以解决 “No Session” 问题了,可以在 Controller 方法中直接返回 Entity,然后再由 Spring 的消息转换器转换为 JSON 了。但是所有返回的 Entity 都需要执行一次上面的 forceLoadObject 方法进行加载才行。既然 Spring 有 AOP 功能,为什么不利用它让 Spring 帮我们执行这种调用呢?

ForceLoadResult 是我自己定义的注解,配置好这个切面后,只需要在 Controller 方法中标注 @ForceLoadResult,然后直接返回 Entity 即可。Spring 会拦截方法返回的结果并且自动帮我们执行 forceLoadObject方法。

RootConfig 的环境配置问题

Spring 支持以 @Profile 指定在不同环境下使用的 Bean,因此,我们可以这样:

你可以通过几种手段,指定应用上下文的当前 profile,我惯用的一种手段是在 DispatcherServlet 的参数中指定:

但这只能对 WebApplicationContext 生效,对于由 ContextLoaderListener 创建的上下文来说,需要另外指定:

 

本文采用 CC BY-NC-SA 3.0 协议进行许可,在您遵循此协议的情况下,可以自由共享与演绎本文章。
本文链接:https://blog.codeingboy.me/questions-about-spring-framework/

发表评论

电子邮件地址不会被公开。 必填项已用*标注