23 答疑现场:Spring 补充篇思考题合集 你好,我是傅健。

欢迎来到第三次答疑现场,恭喜你,到了这,终点已近在咫尺。到今天为止,我们已经解决了 50 个线上问题,是不是很有成就感了?但要想把学习所得真正为你所用还要努力练习呀,这就像理论与实践之间永远有道鸿沟需要我们去跨越一样。那么接下来,话不多说,我们就开始逐一解答第三章的课后思考题了,有任何想法欢迎到留言区补充。

第18课

在案例 1 中使用 Spring Data Redis 时,我们提到了 StringRedisTemplate 和 RedisTemplate。那么它们是如何被创建起来的呢?

实际上,当我们依赖 spring-boot-starter 时,我们就间接依赖了 spring-boot -autoconfigure。

在这个 JAR 中,存在下面这样的一个类,即 RedisAutoConfiguration。 @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(RedisProperties.class) @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(name = “redisTemplate”) @ConditionalOnSingleCandidate(RedisConnectionFactory.class) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean @ConditionalOnSingleCandidate(RedisConnectionFactory.class) public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } }

从上述代码可以看出,当存在RedisOperations这个类时,就会创建 StringRedisTemplate 和 RedisTemplate 这两个 Bean。顺便说句,这个 RedisOperations 是位于 Spring Data Redis 这个 JAR 中。

再回到开头,RedisAutoConfiguration 是如何被发现的呢?实际上,它被配置在

spring-boot-autoconfigure 的 META-INF/spring.factories 中,示例如下: org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\ org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\

那么它是如何被加载进去的呢?我们的应用启动程序标记了@SpringBootApplication,这个注解继承了下面这个注解:

//省略其他非关键代码 @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { //省略其他非关键代码 }

当它使用了 AutoConfigurationImportSelector 这个类,这个类就会导入在META-INF/spring.factories定义的 RedisAutoConfiguration。那么 import 动作是什么时候执行的呢?实际上是在启动应用程序时触发的,调用堆栈信息如下:

结合上面的堆栈和相关源码,我们不妨可以总结下 RedisTemplate 被创建的过程。

当 Spring 启动时,会通过 ConfigurationClassPostProcessor 尝试处理所有标记@Configuration 的类,具体到每个配置类的处理是通过 ConfigurationClassParser 来完成的。

在这个完成过程中,它会使用 ConfigurationClassParser.DeferredImportSelectorHandler 来完成对 Import 的处理。AutoConfigurationImportSelector 就是其中一种Import,它被 @EnableAutoConfiguration 这个注解间接引用。它会加载”META-INF/spring.factories”中定义的 RedisAutoConfiguration,此时我们就会发现 StringRedisTemplate 和 RedisTemplate 这两个 Bean 了。

第19课

RuntimeException 是 Exception 的子类,如果用 rollbackFor=Exception.class,那对 RuntimeException 也会生效。如果我们需要对 Exception 执行回滚操作,但对于 RuntimeException 不执行回滚操作,应该怎么做呢?

我们可以同时为 @Transactional 指定rollbackFor 和noRollbackFor 属性,具体代码示例如下: @Transactional(rollbackFor = Exception.class, noRollbackFor = RuntimeException.class) public void doSaveStudent(Student student) throws Exception { studentMapper.saveStudent(student); if (student.getRealname().equals(“小明”)) { throw new RuntimeException(“该用户已存在”); } }

第20课

结合案例2,请你思考这样一个问题:在这个案例中,我们在 CardService类方法上声明了这样的事务传播属性,@Transactional(propagation = Propagation.REQUIRES_NEW),如果使用 Spring 的默认声明行不行,为什么?

答案是不行。我们前面说过,Spring 默认的事务传播类型是 REQUIRED,在有外部事务的情况下,内部事务则会加入原有的事务。如果我们声明成 REQUIRED,当我们要操作 card 数据的时候,持有的依然还会是原来的 DataSource。

第21课

当我们比较案例 1 和案例 2,你会发现不管使用的是查询(Query)参数还是表单(Form)参数,我们的接口定义并没有什么变化,风格如下: @RestController public class HelloWorldController { @RequestMapping(path = “hi”, method = RequestMethod.GET) public String hi(@RequestParam(“para1”) String para1){ return “helloworld:” + para1; }; }

那是不是 @RequestParam 本身就能处理这两种数据呢?

不考虑实现原理,如果我们仔细看下 @RequestParam 的 API 文档,你就会发现@RequestParam 不仅能处理表单参数,也能处理查询参数。API 文档如下: In Spring MVC, “request parameters” map to query parameters, form data, and parts in multipart requests. This is because the Servlet API combines query parameters and form data into a single map called “parameters”, and that includes automatic parsing of the request body.

稍微深入一点的话,我们还可以从源码上看看具体实现。

不管是使用 Query 参数还是用 Form 参数来访问,对于案例程序而言,解析的关键逻辑都是类似的,都是通过下面的调用栈完成参数的解析:

这里可以看出,负责解析的都是 RequestParamMethodArgumentResolver,解析最后的调用也都是一样的方法。在 org.apache.catalina.connector.Request/#parseParameters 这个方法中,对于 From 的解析是这样的: if (!(“application/x-www-form-urlencoded”.equals(contentType))) { success = true; return; } //走到这里,说明是 Form: “application/x-www-form-urlencoded” int len = getContentLength(); if (len > 0) { int maxPostSize = connector.getMaxPostSize(); if ((maxPostSize >= 0) && (len > maxPostSize)) { //省略非关键代码 } byte[] formData = null; if (len < CACHED_POST_LEN) { if (postData == null) { postData = new byte[CACHED_POST_LEN]; } formData = postData; } else { formData = new byte[len]; } try { if (readPostBody(formData, len) != len) { parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE); return; } } catch (IOException e) { //省略非关键代码 } //把 Form 数据添加到 parameter 里面去 parameters.processParameters(formData, 0, len);

Form 的数据最终存储在 Parameters/#paramHashValues 中。

而对于查询参数的处理,同样是在 org.apache.catalina.connector.Request/#parseParameters 中,不过处理它的代码行在 Form 前面一些,关键调用代码行如下: parameters.handleQueryParameters();

最终它也是通过 org.apache.tomcat.util.http.Parameters/#processParameters 来完成数据的添加。自然,它存储的位置也是 Parameters/#paramHashValues 中。

综上可知,虽然使用的是一个固定的注解 @RequestParam,但是它能处理表单和查询参数,因为它们都会存储在同一个位置:Parameters/#paramHashValues。

第22课

在案例 1 中,我们解释了为什么测试程序加载不到 spring.xml 文件,根源在于当使用下面的语句加载文件时,它们是采用不同的 Resource 形式来加载的: @ImportResource(locations = {“spring.xml”})

具体而言,应用程序加载使用的是 ClassPathResource,测试加载使用的是 ServletContextResource,那么这是怎么造成的呢?

实际上,以何种类型的Resource加载是由 DefaultResourceLoader/#getResource 来决定的: @Override public Resource getResource(String location) { //省略非关键代码 if (location.startsWith(“/”)) { return getResourceByPath(location); } else if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); } else { try { // Try to parse the location as a URL… URL url = new URL(location); return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); } catch (MalformedURLException ex) { // No URL -> resolve as resource path. return getResourceByPath(location); } } }

结合上述代码,你可以看出,当使用下面语句时:

@ImportResource(locations = {“classpath:spring.xml”})

走入的分支是:

//CLASSPATH_URL_PREFIX:classpath else if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); }

即创建的是 ClassPathResource。

而当使用下面语句时: @ImportResource(locations = {“spring.xml”})

走入的分支是:

try { // 按 URL 加载 URL url = new URL(location); return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); } catch (MalformedURLException ex) { // 按路径加载 return getResourceByPath(location); }

先尝试按 URL 加载,很明显这里会失败,因为字符串spring.xml并非一个 URL。随后使用 getResourceByPath()来加载,它会执行到下面的 WebApplicationContextResourceLoader/#getResourceByPath():

private static class WebApplicationContextResourceLoader extends ClassLoaderFilesResourcePatternResolver.ApplicationContextResourceLoader { private final WebApplicationContext applicationContext; //省略非关键代码 protected Resource getResourceByPath(String path) { return (Resource)(this.applicationContext.getServletContext() != null ? new ServletContextResource(this.applicationContext.getServletContext(), path) : super.getResourceByPath(path)); } }

可以看出,这个时候其实已经和 ApplicationContext 息息相关了。在我们的案例中,最终返回的是 ServletContextResource。

相信看到这里,你就能明白为什么一个小小的改动会导致生成的Resource不同了。无非还是因为你定义了不同的格式,不同的格式创建的资源不同,加载逻辑也不同。至于后续是如何加载的,你可以回看全文。

以上就是这次答疑的全部内容,我们下节课再见!

参考资料

https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/23%20%e7%ad%94%e7%96%91%e7%8e%b0%e5%9c%ba%ef%bc%9aSpring%20%e8%a1%a5%e5%85%85%e7%af%87%e6%80%9d%e8%80%83%e9%a2%98%e5%90%88%e9%9b%86.md