通用接口开放平台设计与实现——(31)API服务线程安全问题确认与修复

背景

在本系列的前面一篇博客评论中,有小伙伴指出,API服务存在线程安全问题:

https://blog.csdn.net/seawaving/article/details/122905199#comments_34477405

今天来确认下,线程是否安全?如不安全,如何修复?

回顾

先来回顾下先前的实现,可能存在线程安全的是自己实现的过滤器链,如下:

java 复制代码
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实现的,源码如下:

java 复制代码
/*
 * 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服务中,如下:

java 复制代码
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并不需要持有请求对象和响应对象,去除掉后,就从根本上解决了线程安全问题,调整如下:

java 复制代码
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对象,不使用依赖注入,如下;

java 复制代码
@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注解,从而也不再是单例模式,调整构造方法,以及内部获取类的方式,如下:

java 复制代码
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
如果您在阅读本文时获得了帮助或受到了启发,希望您能够喜欢并收藏这篇文章,为它点赞~
请在评论区与我分享您的想法和心得,一起交流学习,不断进步,遇见更加优秀的自己!

相关推荐
hanxiaozhang201817 小时前
Netty面试重点-2
面试·netty
9527出列2 天前
Netty源码分析--客户端连接接入流程解析
网络协议·netty
马尚来3 天前
【韩顺平】尚硅谷Netty视频教程
后端·netty
马尚道5 天前
【韩顺平】尚硅谷Netty视频教程
netty
马尚道5 天前
Netty核心技术及源码剖析
源码·netty
moxiaoran57535 天前
java接收小程序发送的protobuf消息
websocket·netty·protobuf
马尚来6 天前
尚硅谷 Netty核心技术及源码剖析 Netty模型 详细版
源码·netty
马尚来6 天前
Netty核心技术及源码剖析
后端·netty
失散1312 天前
分布式专题——35 Netty的使用和常用组件辨析
java·分布式·架构·netty
hanxiaozhang201814 天前
Netty面试重点-1
网络·网络协议·面试·netty