背景
在本系列的前面一篇博客评论中,有小伙伴指出,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 {
@Nullable
private ServletRequest request;
@Nullable
private ServletResponse response;
private final List<Filter> filters;
@Nullable
private 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.
*/
@Nullable
public ServletRequest getRequest() {
return this.request;
}
/**
* Return the response that {@link #doFilter} has been called with.
*/
@Nullable
public ServletResponse getResponse() {
return this.response;
}
/**
* Invoke registered {@link Filter Filters} and/or {@link Servlet} also saving the
* request and response.
*/
@Override
public 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;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
this.delegateServlet.service(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
@Override
public 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;
@Autowired
private ApiServiceLogService apiServiceLogService;
public ApiRestServiceImpl(FrameworkValidateFilter frameworkValidateFilter,
BusinessFilter businessFilter, BasicValidateFilter basicValidateFilter) {
filterChain = new ApiFilterChain(basicValidateFilter, frameworkValidateFilter, businessFilter);
}
@Override
public 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")
@AllowAll
public 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);
}
@Override
public 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
如果您在阅读本文时获得了帮助或受到了启发,希望您能够喜欢并收藏这篇文章,为它点赞~
请在评论区与我分享您的想法和心得,一起交流学习,不断进步,遇见更加优秀的自己!