背景
在本系列的前面一篇博客评论中,有小伙伴指出,API服务存在线程安全问题:
https://blog.csdn.net/seawaving/article/details/122905199#comments_34477405
今天来确认下,线程是否安全?如不安全,如何修复?
回顾
先来回顾下先前的实现,可能存在线程安全的是自己实现的过滤器链,如下:
package tech.abc.platform.cip.api.framework;import tech.abc.platform.cip.common.entity.ApiRequest;
import tech.abc.platform.cip.common.entity.ApiResponse;import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;/*** API服务过滤器链条** @author wqliu* @date 2022-2-12**/
public class ApiFilterChain {/*** 请求*/private ApiRequest request;/*** 响应*/private ApiResponse response = new ApiResponse();/*** 过滤器集合*/private final List<ApiFilter> filters;/*** 过滤器迭代器*/private Iterator<ApiFilter> iterator;public ApiFilterChain() {filters = Collections.EMPTY_LIST;}public ApiFilterChain(ApiFilter... filters) {this.filters = Arrays.asList(filters);}/*** 获取请求** @return {@link ApiRequest}*/public ApiRequest getRequest() {return this.request;}/*** 获取响应** @return {@link ApiResponse}*/public ApiResponse getResponse() {return this.response;}/*** 执行过滤** @param request 请求* @param response 响应*/public void doFilter(ApiRequest request, ApiResponse response) {// 如迭代器为空,则初始化if (this.iterator == null) {this.iterator = this.filters.iterator();}// 集合中还有过滤器,则继续往下传递if (this.iterator.hasNext()) {ApiFilter nextFilter = this.iterator.next();nextFilter.doFilter(request, response, this);}// 将处理结果更新到属性中this.request = request;this.response = response;}/*** 重置*/public void reset() {this.request = null;this.response = null;this.iterator = null;}}
该类是参照官方的MockFilterChain
实现的,源码如下:
/** Copyright 2002-2018 the original author or authors.** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** https://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package org.springframework.mock.web;import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;/*** Mock implementation of the {@link javax.servlet.FilterChain} interface.** <p>A {@link MockFilterChain} can be configured with one or more filters and a* Servlet to invoke. The first time the chain is called, it invokes all filters* and the Servlet, and saves the request and response. Subsequent invocations* raise an {@link IllegalStateException} unless {@link #reset()} is called.** @author Juergen Hoeller* @author Rob Winch* @author Rossen Stoyanchev* @since 2.0.3* @see MockFilterConfig* @see PassThroughFilterChain*/
public class MockFilterChain implements FilterChain {@Nullableprivate ServletRequest request;@Nullableprivate ServletResponse response;private final List<Filter> filters;@Nullableprivate Iterator<Filter> iterator;/*** Register a single do-nothing {@link Filter} implementation. The first* invocation saves the request and response. Subsequent invocations raise* an {@link IllegalStateException} unless {@link #reset()} is called.*/public MockFilterChain() {this.filters = Collections.emptyList();}/*** Create a FilterChain with a Servlet.* @param servlet the Servlet to invoke* @since 3.2*/public MockFilterChain(Servlet servlet) {this.filters = initFilterList(servlet);}/*** Create a {@code FilterChain} with Filter's and a Servlet.* @param servlet the {@link Servlet} to invoke in this {@link FilterChain}* @param filters the {@link Filter}'s to invoke in this {@link FilterChain}* @since 3.2*/public MockFilterChain(Servlet servlet, Filter... filters) {Assert.notNull(filters, "filters cannot be null");Assert.noNullElements(filters, "filters cannot contain null values");this.filters = initFilterList(servlet, filters);}private static List<Filter> initFilterList(Servlet servlet, Filter... filters) {Filter[] allFilters = ObjectUtils.addObjectToArray(filters, new ServletFilterProxy(servlet));return Arrays.asList(allFilters);}/*** Return the request that {@link #doFilter} has been called with.*/@Nullablepublic ServletRequest getRequest() {return this.request;}/*** Return the response that {@link #doFilter} has been called with.*/@Nullablepublic ServletResponse getResponse() {return this.response;}/*** Invoke registered {@link Filter Filters} and/or {@link Servlet} also saving the* request and response.*/@Overridepublic void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {Assert.notNull(request, "Request must not be null");Assert.notNull(response, "Response must not be null");Assert.state(this.request == null, "This FilterChain has already been called!");if (this.iterator == null) {this.iterator = this.filters.iterator();}if (this.iterator.hasNext()) {Filter nextFilter = this.iterator.next();nextFilter.doFilter(request, response, this);}this.request = request;this.response = response;}/*** Reset the {@link MockFilterChain} allowing it to be invoked again.*/public void reset() {this.request = null;this.response = null;this.iterator = null;}/*** A filter that simply delegates to a Servlet.*/private static final class ServletFilterProxy implements Filter {private final Servlet delegateServlet;private ServletFilterProxy(Servlet servlet) {Assert.notNull(servlet, "servlet cannot be null");this.delegateServlet = servlet;}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {this.delegateServlet.service(request, response);}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void destroy() {}@Overridepublic String toString() {return this.delegateServlet.toString();}}}
这个类用在了我们API服务中,如下:
package tech.abc.platform.cip.api.service.impl;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tech.abc.platform.cip.api.exception.ApiException;
import tech.abc.platform.cip.api.framework.ApiFilterChain;
import tech.abc.platform.cip.api.framework.BasicValidateFilter;
import tech.abc.platform.cip.api.framework.BusinessFilter;
import tech.abc.platform.cip.api.framework.FrameworkValidateFilter;
import tech.abc.platform.cip.api.service.ApiRestService;
import tech.abc.platform.cip.common.entity.ApiRequest;
import tech.abc.platform.cip.common.entity.ApiResponse;
import tech.abc.platform.cip.enums.ApiServiceExecuteResultEnum;
import tech.abc.platform.cip.service.ApiServiceLogService;
import tech.abc.platform.common.exception.CustomException;import java.time.LocalDateTime;/*** '* API服务技术框架实现** @author wqliu* @date 2022-2-10**/
@Service
public class ApiRestServiceImpl implements ApiRestService {private ApiFilterChain filterChain;@Autowiredprivate ApiServiceLogService apiServiceLogService;public ApiRestServiceImpl(FrameworkValidateFilter frameworkValidateFilter,BusinessFilter businessFilter, BasicValidateFilter basicValidateFilter) {filterChain = new ApiFilterChain(basicValidateFilter, frameworkValidateFilter, businessFilter);}@Overridepublic ApiResponse handle(ApiRequest apiRequest) {LocalDateTime receiveTime = LocalDateTime.now();ApiResponse apiResponse = new ApiResponse();try {filterChain.doFilter(apiRequest, apiResponse);apiResponse = this.filterChain.getResponse();apiResponse.setExecuteResult(ApiServiceExecuteResultEnum.SUCCESS.name());} catch (CustomException ex) {// 自定义异常处理apiResponse.setExecuteResult(ApiServiceExecuteResultEnum.ERROR.name());apiResponse.setErrorCode("S00");apiResponse.setErrorMessage(ex.getMessage());} catch (ApiException ex) {// API异常处理apiResponse.setExecuteResult(ApiServiceExecuteResultEnum.ERROR.name());apiResponse.setErrorCode(ex.getErrorCode());apiResponse.setErrorMessage(ex.getMessage());} catch (Exception ex) {// 非预期异常处理apiResponse.setExecuteResult(ApiServiceExecuteResultEnum.ERROR.name());apiResponse.setErrorCode("S99");apiResponse.setErrorMessage("未定义异常:" + ex.getMessage());} finally {// 需要重置,为下次请求服务filterChain.reset();// 记录日志apiServiceLogService.recordLog(apiRequest, apiResponse, receiveTime);}return apiResponse;}
}
分析
其中ApiRestServiceImpl使用的@Service注解,Spring的默认处理是单例模式,ApiFilterChain是在构造函数中new出来的。线程安全的关键点,在于ApiFilterChain持有和保存了ApiRequest和ApiResponse对象。
从代码层面分析了一下,ApiFilterChain确实没有持有ApiRequest和ApiResponse对象的必要,通过方法接收ApiRequest对象,然后处理过程中修改ApiResponse对象,都是引用,没必要再保存一份。
至于当时为什么这么写,大概是参照官方MockFilterChain写法,高度信任而没有深度思考是否需要这么做。
验证
上面是从代码层面分析,接下来就动手验证下,线程安全问题是否存在,借此也夯实下实际工作中很少用到的多线程并发及线程安全基础。
如何验证呢?
其实思路也挺简单,发起多次接口调用,通过日志输出对象的HashCode,看看HashCode是否是同一个就好了。
使用Postman来做接口测试,调用平台内置的查询待处理消息的服务接口platform.message.query,如下:
注:为方便测试,把签名验证处理临时注释掉了,因此入参sign属性随便写了个1111,以通过非空验证。
在服务接口处理中添加日志输出,这里用error而不是info目的是更容易找到,如下:
然后使用postman发起两次接口调用,查看日志,如下:
可以看到,无论是ApiRestService,还是其所属的filterChain,哈希码是完全相同的,已经足以说明就是同一个对象,因此存在线程安全问题。当接口同时收到多个请求时,也就是多线程并发时,持有的请求对象和响应对象会混乱掉,是个大问题。
修正
既然问题已经确认,那接下来就修正它。
在前面分析环节,实际已经分析出来,filterChain并不需要持有请求对象和响应对象,去除掉后,就从根本上解决了线程安全问题,调整如下:
package tech.abc.platform.cip.api.framework;import tech.abc.platform.cip.common.entity.ApiRequest;
import tech.abc.platform.cip.common.entity.ApiResponse;import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;/*** API服务过滤器链条** @author wqliu* @date 2022-2-12**/
public class ApiFilterChain {/*** 过滤器集合*/private final List<ApiFilter> filters;/*** 过滤器迭代器*/private Iterator<ApiFilter> iterator;public ApiFilterChain() {filters = Collections.EMPTY_LIST;}public ApiFilterChain(ApiFilter... filters) {this.filters = Arrays.asList(filters);}/*** 执行过滤** @param request 请求* @param response 响应*/public void doFilter(ApiRequest request, ApiResponse response) {// 如迭代器为空,则初始化if (this.iterator == null) {this.iterator = this.filters.iterator();}// 集合中还有过滤器,则继续往下传递if (this.iterator.hasNext()) {ApiFilter nextFilter = this.iterator.next();nextFilter.doFilter(request, response, this);}}/*** 重置*/public void reset() {this.iterator = null;}}
测试功能正常,如下:
新的疑问点
在调整ApiFilterChain的过程中,去除了存在线程安全的ApiRequest和ApiResponse对象,同时发现还持有一个过滤器集合对象
private final List filters,该对象在构造方法中初始化,在接口服务的finally里执行reset清理工作,不过reset方法是重置过滤器集合的迭代器,而不是清空过滤器集合本身。
假设是多线程并发情况下,A、B两个请求先后到达,A请求处理结束了,调用reset清空了过滤器的迭代器,而B请求还在只走完了3个过滤器中的2个,会不会有问题呢?
按照前面的验证方法,输出哈希码,确定是同一个对象。
要做并发测试,比较麻烦,得辅助Jmeter等工具来实现了
这个地方高度怀疑存在线程安全问题,比较彻底的解决办法,就是把API服务变更为非单例模式。
彻底改造
接口服务对应的控制器中直接new对象,不使用依赖注入,如下;
@RestController
@RequestMapping("/api")
@Slf4j
public class ApiRestController {@PostMapping("/rest")@AllowAllpublic ResponseEntity<ApiResponse> post(@RequestBody ApiRequest apiRequest) {ApiRestService apiRestService = new ApiRestServiceImpl();ApiResponse apiResponse = apiRestService.handle(apiRequest);return new ResponseEntity<ApiResponse>(apiResponse, HttpStatus.OK);}}
ApiRestServiceImpl去除@Service注解,从而也不再是单例模式,调整构造方法,以及内部获取类的方式,如下:
package tech.abc.platform.cip.api.service.impl;import lombok.extern.slf4j.Slf4j;
import tech.abc.platform.cip.api.exception.ApiException;
import tech.abc.platform.cip.api.framework.ApiFilterChain;
import tech.abc.platform.cip.api.framework.BasicValidateFilter;
import tech.abc.platform.cip.api.framework.BusinessFilter;
import tech.abc.platform.cip.api.framework.FrameworkValidateFilter;
import tech.abc.platform.cip.api.service.ApiRestService;
import tech.abc.platform.cip.common.entity.ApiRequest;
import tech.abc.platform.cip.common.entity.ApiResponse;
import tech.abc.platform.cip.enums.ApiServiceExecuteResultEnum;
import tech.abc.platform.cip.service.ApiServiceLogService;
import tech.abc.platform.common.exception.CustomException;
import tech.abc.platform.common.utils.SpringUtil;import java.time.LocalDateTime;/*** '* API服务技术框架实现** @author wqliu* @date 2022-2-10**/@Slf4j
public class ApiRestServiceImpl implements ApiRestService {private ApiFilterChain filterChain;public ApiRestServiceImpl() {BusinessFilter businessFilter = SpringUtil.getBean(BusinessFilter.class);FrameworkValidateFilter frameworkValidateFilter = SpringUtil.getBean(FrameworkValidateFilter.class);BasicValidateFilter basicValidateFilter = SpringUtil.getBean(BasicValidateFilter.class);filterChain = new ApiFilterChain(basicValidateFilter, frameworkValidateFilter, businessFilter);}@Overridepublic ApiResponse handle(ApiRequest apiRequest) {LocalDateTime receiveTime = LocalDateTime.now();ApiResponse apiResponse = new ApiResponse();try {filterChain.doFilter(apiRequest, apiResponse);apiResponse.setExecuteResult(ApiServiceExecuteResultEnum.SUCCESS.name());} catch (CustomException ex) {// 自定义异常处理apiResponse.setExecuteResult(ApiServiceExecuteResultEnum.ERROR.name());apiResponse.setErrorCode("S00");apiResponse.setErrorMessage(ex.getMessage());} catch (ApiException ex) {// API异常处理apiResponse.setExecuteResult(ApiServiceExecuteResultEnum.ERROR.name());apiResponse.setErrorCode(ex.getErrorCode());apiResponse.setErrorMessage(ex.getMessage());} catch (Exception ex) {// 非预期异常处理apiResponse.setExecuteResult(ApiServiceExecuteResultEnum.ERROR.name());apiResponse.setErrorCode("S99");apiResponse.setErrorMessage("未定义异常:" + ex.getMessage());} finally {ApiServiceLogService apiServiceLogService = SpringUtil.getBean(ApiServiceLogService.class);// 记录日志apiServiceLogService.recordLog(apiRequest, apiResponse, receiveTime);}return apiResponse;}
}
运行,发现多次接口调用进行测试,每次接口调用,无论是ApiRestService还是ApiFilterChain,都不是同一个对象了,因此线程肯定是安全的了。
开源平台资料
平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:[csdn专栏]
开源地址:[Gitee]
开源协议:MIT
如果您在阅读本文时获得了帮助或受到了启发,希望您能够喜欢并收藏这篇文章,为它点赞~
请在评论区与我分享您的想法和心得,一起交流学习,不断进步,遇见更加优秀的自己!