一、使用Spring出错根源
1、隐式规则的存在
你可能忽略了 Sping Boot 中 @SpringBootApplication 是有一个默认的扫描包范围的。这就是一个隐私规则。如果你原本不知道,那么犯错概率还是很高的。类似的案例这里不再赘述。
2、默认配置不合理
3、追求奇技淫巧
4、理所当然地使用
要完整接收到所有的 Header,不能直接使用 Map 而应该使用 MultiValueMap。
5、无关的依赖变动
你一定要意识到,当你的代码不变时,你的依赖变了,行为则可能“异常”了。
6、例如默认扫描 Bean 的范围、自动装配构造器等等
7、定义了多个构造器就可能报错,因为使用反射方式来创建实例必须要明确使用的是哪一个构造器
8、当一个单例的 Bean,使用 autowired 注解标记其属性时,你一定要注意这个属性值会被固定下来。
解决方案:
解决方案一:自动注入 Context
即自动注入 ApplicationContext,然后定义 getServiceImpl() 方法,在方法中获取一个新的 ServiceImpl 类型实例。修正代码如下:
@RestController
public class HelloWorldController {@Autowiredprivate ApplicationContext applicationContext;@RequestMapping(path = "hi", method = RequestMethod.GET)public String hi(){return "helloworld, service is : " + getServiceImpl();};public ServiceImpl getServiceImpl(){return applicationContext.getBean(ServiceImpl.class);}}
解决方案二: 使用 Lookup 注解
@RestController
public class HelloWorldController {@RequestMapping(path = "hi", method = RequestMethod.GET)public String hi(){return "helloworld, service is : " + getServiceImpl();};@Lookuppublic ServiceImpl getServiceImpl(){return null;} }
二、找不到合适的 Bean,但是原因却不尽相同。
1、提供的 Bean 过多又无法决策选择谁;
解决方案:精确匹配
@Autowired
DataService oracleDataService;
2、是因为指定的名称不规范导致引用的 Bean 找不到。
解决方案:定义处显式指定 Bean 名字,我们可以保持引用代码不变,而通过显式指明 CassandraDataService 的 Bean 名称为 CassandraDataService 来纠正这个问题。
@Repository("CassandraDataService")
@Slf4j
public class CassandraDataService implements DataService {//省略实现
}
3、引用内部类的 Bean 遗忘类名
解决方案:全部名称引入
@Autowired
@Qualifier("studentController.InnerClassDataService")
DataService innerClassDataService;
4、 @Value 不仅可以用来注入 String 类型,也可以注入自定义对象类型。同时在注入 String 时,你一定要意识到它不仅仅可以用来引用配置文件里配置的值,也可能引用到环境变量、系统参数等。
5、我们了解到集合类型的注入支持两种常见的方式,即上文中我们命名的收集装配式和直接装配式。这两种方式共同装配一个属性时,后者就会失效。
解决方案:只用一个
直接装配
@Bean
public List<Student> students(){Student student1 = createStudent(1, "xie");Student student2 = createStudent(2, "fang");Student student3 = createStudent(3, "liu");Student student4 = createStudent(4, "fu");return Arrays.asList(student1,student2,student3, student4);
}
三、Spring Bean 生命周期常见错误
Spring 初始化单例类的一般过程,基本都是 getBean()->doGetBean()->getSingleton(),如果发现 Bean 不存在,则调用 createBean()->doCreateBean() 进行实例化。
1、DefaultListableBeanFactory 类是 Spring Bean 的灵魂,而核心就是其中的 doCreateBean 方法,它掌控了 Bean 实例的创建、Bean 对象依赖的注入、定制类初始化方法的回调以及 Disposable 方法的注册等全部关键节点。
2、后置处理器是 Spring 中最优雅的设计之一,对于很多功能注解的处理都是借助于后置处理器来完成的。Bean 对象“补充”初始化动作却是在 CommonAnnotationBeanPostProcessor(继承自 InitDestroyAnnotationBeanPostProcessor)这个后置处理器中完成的。
3、构造器内抛空指针异常
场景:
在构建宿舍管理系统时,有 LightMgrService 来管理 LightService,从而控制宿舍灯的开启和关闭。我们希望在 LightMgrService 初始化时能够自动调用 LightService 的 check 方法来检查所有宿舍灯的电路是否正常
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService {@Autowiredprivate LightService lightService;public LightMgrService() {lightService.check();}
}
错误:
LightService 的 check报错NullPointerException
原因:
使用 @Autowired 直接标记在成员属性上而引发的装配行为是发生在构造器执行之后的。
解决方案:
使用构造器参数来隐式注入是一种 Spring 最佳实践
@Component
public class LightMgrService {private LightService lightService;public LightMgrService(LightService lightService) {this.lightService = lightService;lightService.check();}
}
4、意外触发 shutdown 方法
原因:避免在 Java 类中定义一些带有特殊意义动词的方法来解决
解决方案:
方案一:
避免使用close 或者 shutdown 方法
方案二:
也可以通过将 Bean 注解内 destroyMethod 属性设置为空的方式来解决这个问题。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfiguration {@Bean(destroyMethod="")public LightService getTransmission(){return new LightService();}
}
四、Spring AOP 常见错误
1、AOP 本质上就是一个代理模式:Spring AOP 则利用 CGlib 和 JDK 动态代理等方式来实现运行期动态方法增强,其目的是将与业务无关的代码单独抽离出来,使其逻辑不再与业务代码耦合,从而降低系统的耦合性,提高程序的可重用性和开发效率。
2、AOP 便成为了日志记录、监控管理、性能统计、异常处理、权限管理、统一认证等各个方面被广泛使用的技术。
3、Spring AOP 的实现
Spring AOP 的底层是动态代理。而创建代理的方式有两种,JDK 的方式和 CGLIB 的方式。
JDK 动态代理只能对实现了接口的类生成代理,而不能针对普通类。而 CGLIB 是可以针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,来实现代理对象。具体区别可参考下图:
4、如何使用 Spring AOP
pom.xml
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
在需要使用 AOP 时,它会把创建的原始的 Bean 对象 wrap 成代理对象作为 Bean 返回。
createProxy 调用是创建代理对象的关键。具体到执行过程,它首先会创建一个代理工厂,然后将通知器(advisors)、被代理对象等信息加入到代理工厂,最后通过这个代理工厂来获取代理对象。
5、this 调用的当前类方法无法被拦截
原因:只有引用的是被动态代理创建出来的对象,才会被 Spring 增强,具备 AOP 该有的功能。
解决方案:
通过 @Autowired 的方式,在类的内部,自己引用自己:
@Service
public class ElectricService {@AutowiredElectricService electricService;public void charge() throws Exception {System.out.println("Electric charging ...");//this.pay();electricService.pay();}public void pay() throws Exception {System.out.println("Pay with alipay ...");Thread.sleep(1000);}
}
6、我们一般不能直接从代理类中去拿被代理类的属性
原因:这是因为除非我们显示设置 spring.objenesis.ignore 为 true,否则代理类的属性是不会被 Spring 初始化的
解决方案:我们可以通过在被代理类中增加一个方法来间接获取其属性。
7、在同一个切面配置中,如果存在多个不同类型的增强的执行顺序:
执行优先级:按照增强类型的特定顺序排列,依次的增强类型为 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class;
8、在同一个切面配置中,如果存在多个相同类型的增强的执行顺序:
执行优先级:按照该增强的方法名排序,排序方式依次为比较方法名的每一个字母,直到发现第一个不相同且 ASCII 码较小的字母。
9、比较器相关的知识
① 任意两个比较器(Comparator)可以通过 thenComparing() 连接合成一个新的连续比较器;
② 比较器的比较规则有一个简单的方法可以帮助你理解,就是最终一定需要对象两两比较,而比较的过程一定是比较这两个对象的同种属性。你只要抓住这两点:比较了什么属性以及比较的结果是什么就可以了,若比较结果为正数,则按照该属性的升序排列;若为负数,则按属性降序排列。
10、Spring事件常见错误
① 误读事件本身含义;
② 监听错了事件的传播系统;
③ 事件处理之间互相影响,导致部分事件处理无法完成。
五、Spring web
1、Spring Boot 处理一个 HTTP 请求的核心过程,无非就是绑定一个内嵌容器(Tomcat/Jetty/ 其他)来接收请求,然后为请求寻找一个合适的方法,最后反射执行它。
2、当我们使用 @PathVariable 时,一定要注意传递的值是不是含有 / ;
3、当我们使用 @RequestParam、@PathVarible 等注解时,一定要意识到一个问题,虽然下面这两种方式(以 @RequestParam 使用示例)都可以,但是后者在一些项目中并不能正常工作,因为很多产线的编译配置会去掉不是必须的调试信息。
@RequestMapping(path = "/hi1", method = RequestMethod.GET)public String hi1(@RequestParam("name") String name){ return name;};
4、任何一个参数,我们都需要考虑它是可选的还是必须的。同时,你一定要想到参数类型的定义到底能不能从请求中自动转化而来。Spring 本身给我们内置了很多转化器,但是我们要以合适的方式使用上它。另外,Spring 对很多类型的转化设计都很贴心,例如使用下面的注解就能解决自定义日期格式参数转化问题。
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") Date date
5、要完整接收到所有的 Header,不能直接使用 Map 而应该使用 MultiValueMap。常见的两种方式如下:
//方式 1
@RequestHeader() MultiValueMap map
//方式 2:专用于Header的MultiValueMap子类型
@RequestHeader() HttpHeaders map
深究原因,Spring 在底层解析 Header 时如果接收参数是 Map,则当请求的 Header 是多 Value 时,只存下了其中一个 Value。
6、在 HTTP 协议规定中,Header 的名称是无所谓大小写的。但是这并不意味着所有能获取到 Header 的途径,最终得到的 Header 名称都是统一大小写的。
7、不是所有的 Header 在响应中都能随意指定,虽然表面看起来能生效,但是最后返回给客户端的仍然不是你指定的值。例如,在 Tomcat 下,CONTENT_TYPE 这个 Header 就是这种情况。
8、不同的 Body 需要不同的编解码器,而使用哪一种是协商出来的,协商过程大体如下:
查看请求头中是否有 ACCEPT 头,如果没有则可以使用任何类型;
查看当前针对返回类型(即 Student 实例)可以采用的编码类型;
取上面两步获取的结果的交集来决定用什么方式返回。
9、不同的编解码器的实现(例如 JSON 工具 Jaskson 和 Gson)可能有一些细节上的不同,所以你一定要注意当依赖一个新的 JAR 时,是否会引起默认编解码器的改变,从而影响到一些局部行为的改变。
10、在尝试读取 HTTP Body 时,你要注意到 Body 本身是一个流对象,不能被多次读取。
11、在非 Spring Boot 程序中,JSON 等编解码器不见得是内置好的,需要添加相关的 JAR 才能自动依赖上,而自动依赖的实现是通过检查 Class 是否存在来实现的:当依赖上相关的 JAR 后,关键的 Class 就存在了,响应的编解码器功能也就提供上了。
12、对象参数校验失效
解决方案:
public void addStudent(@Validated @RequestBody Student student)
13、嵌套校验失效
@Valid
private Phone phone;
14、误解校验执行
@NotEmpty
@Size(min = 1, max = 10)
private String name;
六、Spring Web 过滤器使用常见错误
1、过滤器不生效
@WebFilter 这种方式构建的 Filter 是无法直接根据过滤器定义类型来自动注入的,因为这种 Filter 本身是以内部 Bean 来呈现的,它最终是通过 FilterRegistrationBean 来呈现给 Spring 的。所以我们可以通过自动注入 FilterRegistrationBean 类型来完成装配工作,示例如下:
@Autowired@Qualifier("com.spring.puzzle.filter.TimeCostFilter")FilterRegistrationBean timeCostFilter;
2、多次调用过滤器
我们在过滤器的执行中,一定要注意避免不要多次调用 doFilter(),否则可能会出现业务代码执行多次的问题。这个问题出现的根源往往在于“不小心”,但是要理解这个问题呈现的现象,就必须对过滤器的流程有所了解。可以看过滤器执行的核心流程图:
关键步骤:
① 当一个请求来临时,会执行到 StandardWrapperValve 的 invoke(),这个方法会创建 ApplicationFilterChain,并通过 ApplicationFilterChain#doFilter() 触发过滤器执行;
② ApplicationFilterChain 的 doFilter() 会执行其私有方法 internalDoFilter;
③ 在 internalDoFilter 方法中获取下一个 Filter,并使用 request、response、this(当前 ApplicationFilterChain 实例)作为参数来调用 doFilter():
④ 在 Filter 类的 doFilter() 中,执行 Filter 定义的动作并继续传递,获取第三个参数 ApplicationFilterChain,并执行其 doFilter();
⑤ 此时会循环执行进入第 2 步、第 3 步、第 4 步,直到第 3 步中所有的 Filter 类都被执行完毕为止;
⑥ 所有的 Filter 过滤器都被执行完毕后,会执行 servlet.service(request, response) 方法,最终调用对应的 Controller 层方法 。
3、@WebFilter 和 @Component 的相同点是
① 它们最终都被包装并实例化成为了 FilterRegistrationBean;
② 它们最终都是在 ServletContextInitializerBeans 的构造器中开始被实例化。
4、@WebFilter 和 @Component 的不同点是:
① 被 @WebFilter 修饰的过滤器会被提前在 BeanFactoryPostProcessors 扩展点包装成 FilterRegistrationBean 类型的 BeanDefinition,然后在 ServletContextInitializerBeans.addServletContextInitializerBeans() 进行实例化;而使用 @Component 修饰的过滤器类,是在 ServletContextInitializerBeans.addAdaptableBeans() 中被实例化成 Filter 类型后,再包装为 RegistrationBean 类型。
② 被 @WebFilter 修饰的过滤器不会注入 Order 属性,但被 @Component 修饰的过滤器会在 ServletContextInitializerBeans.addAdaptableBeans() 中注入 Order 属性。
七、Spring Security 常见错误
1、遗忘 PasswordEncoder
在新版本的 Spring Security 中,你一定不要忘记指定一个 PasswordEncoder,因为出于安全考虑,我们肯定是要对密码加密的。至于如何指定,其实有多种方式。常见的方式是自定义一个 PasswordEncoder 类型的 Bean。还有一种不常见的方式是通过存储密码时加上加密方法的前缀来指定,例如密码原来是 password123,指定前缀后可能是 {MD5}password123。我们可以根据需求来采取不同的解决方案。
2、ROLE_ 前缀与角色
在使用角色相关的授权功能时,你一定要注意这个角色是不是加了前缀 ROLE_。
虽然 Spring 在很多角色的设置上,已经尽量尝试加了前缀,但是仍然有许多接口是可以随意设置角色的。所以有时候你没意识到这个问题去随意设置的话,在授权检验时就会出现角色控制不能生效的情况。从另外一个角度看,当你的角色设置失败时,你一定要关注下是不是忘记加前缀了。
八、Spring Exception 常见错误
1、过滤器处理异常
从这张图中可以看出,当所有的过滤器被执行完毕以后,Spring 才会进入 Servlet 相关的处理,而 DispatcherServlet 才是整个 Servlet 处理的核心,它是前端控制器设计模式的实现,提供 Spring Web MVC 的集中访问点并负责职责的分派。正是在这里,Spring 处理了请求和处理器之间的对应关系,以及这个案例我们所关注的问题——统一异常处理。
为了利用 Spring MVC 的异常处理机制,我们需要对 Filter 做一些改造。手动捕获异常,并将异常 HandlerExceptionResolver 进行解析处理。
修改 PermissionFilter,注入 HandlerExceptionResolver:
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
在 doFilter 里捕获异常并交给 HandlerExceptionResolver 处理:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest httpServletRequest = (HttpServletRequest) request;HttpServletResponse httpServletResponse = (HttpServletResponse) response;String token = httpServletRequest.getHeader("token");if (!"111111".equals(token)) {System.out.println("throw NotAllowException");resolver.resolveException(httpServletRequest, httpServletResponse, null, new NotAllowException());return;}chain.doFilter(request, response);}
2、特殊的 404 异常
修改 MyExceptionHandler 的 @ExceptionHandler 为 NoHandlerFoundException 即可:
@ExceptionHandler(NoHandlerFoundException.class)
3、DispatcherServlet 类中的 doDispatch() 是整个 Servlet 处理的核心,它不仅实现了请求的分发,也提供了异常统一处理等等一系列功能;
4、WebMvcConfigurationSupport 是 Spring Web 中非常核心的一个配置类,无论是异常处理器的包装注册(HandlerExceptionResolver),还是资源处理器的包装注册(SimpleUrlHandlerMapping),都是依靠这个类来完成的。
九、Spring Data
1、一定要注意一致性,例如读写的序列化方法需要一致;
2、一定要重新检查下所有的默认配置是什么,是否符合当前的需求,例如在 Spring Data Cassandra 中,默认的一致性级别在大多情况下都不适合;
3、如果你自定义自己的 Session,一定要避免冗余的 Session 产生。
十、Spring事件
1、事件失败不会滚
原因:Spring 在处理事务过程中,并不会对 Exception 进行回滚,而会对 RuntimeException 或者 Error 进行回滚。
解决方案:
方案一:把抛出的异常类型改成 RuntimeException 就可以了
方案二:在 @Transactional 的 rollbackFor 加入需要支持的异常类型(在这里是 Exception)就可以匹配上我们抛出的异常,进而在异常抛出时进行回滚。
@Transactional(rollbackFor = Exception.class)
2、@Transactional 对 private 方法不生效
① 注解为事务的方法被声明为 public 的时候,才会被 Spring 处理。
② 加了事务注解的方法,必须是调用被 Spring AOP 代理过的方法
3、Spring 在事务处理中有一个很重要的属性 Propagation,主要用来配置当前需要执行的方法如何使用事务,以及与其它事务之间的关系。
4、Spring 默认的传播属性是 REQUIRED,在有事务状态下执行,如果当前没有事务,则创建新的事务;
解决方案:REQUIRES_NEW
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void regCourse(int studentId) throws Exception {studentCourseMapper.saveStudentCourse(studentId, 1);courseMapper.addCourseNumber(1);throw new Exception("注册失败");
}
① 当子事务声明为 Propagation.REQUIRES_NEW 时,在 TransactionAspectSupport.invokeWithinTransaction() 中调用 createTransactionIfNecessary() 就会创建一个新的事务,独立于外层事务。
② 而在 AbstractPlatformTransactio
String url = "http://localhost:8080/hi?para1=1#2";
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
URI uri = builder.build().encode().toUri();
nManager.processRollback() 进行 rollback 处理时,因为 status.isNewTransaction() 会因为它处于一个新的事务中而返回 true,所以它走入到了另一个分支,执行了 doRollback() 操作,让这个子事务单独回滚,不会影响到主事务。
十一、Spring Rest Template 常见错误
1、当使用 RestTemplate 组装表单数据时,我们应该注意要使用 MultiValueMap 而非普通的 HashMap。否则会以 JSON 请求体的形式发送请求而非表单,正确示例如下:
MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<String, Object>();
paramMap.add("para1", "001");
paramMap.add("para2", "002");String url = "http://localhost:8080/hi";
String result = template.postForObject(url, paramMap, String.class);
System.out.println(result)
2、当使用 RestTemplate 发送请求时,如果带有查询(Query)参数,我们一定要注意是否含有一些特殊字符(#)。如果有的话,可以使用下面的 URL 组装方式进行规避
String url = "http://localhost:8080/hi?para1=1#2";
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
URI uri = builder.build().encode().toUri();
3、在 RestTemplate 中使用 URL,我们一定要避免多次转化而导致的多次编码问题。
解决方案:
RestTemplate restTemplate = new RestTemplate();
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://localhost:8080/hi");
builder.queryParam("para1", "开发测试 001");
URI url = builder.encode().build().toUri();
ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
System.out.println(forEntity.getBody());
十二、Spring Test 常见错误
1、在使用 Spring Test 的时候,一定要注意资源文件的加载方式是否正确
例如,你使用的是绝对路径,形式如下:
@ImportResource(locations = {"spring.xml"})
那么它可能在不同的场合实现不同,不一定能加载到你想要的文件,所以我并不推荐你在使用 @ImportResource 时,使用绝对路径指定资源。
2、@MockBean 可能会导致 Spring Context 反复新建,从而让测试变得缓慢,从根源上看,这是属于正常现象。
而假设你需要加速,你可以尝试多种方法,例如,你可以把依赖 Mock 的 Bean 声明在一个统一的地方。当然,你要格外注意这样是否还能满足你的测试需求。