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

相关推荐
西京刀客4 天前
BIO、NIO、AIO的区别?
netty·nio·bio
潇雷8 天前
Netty(3)进阶篇|半包粘包、编解码器
java·后端·netty
雪碧聊技术8 天前
05-接口文档、根据接口文档完善登录功能
接口·请求参数·接口文档·url地址·请求方法
矛取矛求12 天前
string接口的深度理解(内附思维导图)
c语言·开发语言·c++·接口·string
SunkingYang18 天前
C#如何封装将函数封装为接口dll?
开发语言·c#·接口·dll·封装
WaaTong18 天前
Netty 组件介绍 - ByteBuf
java·开发语言·netty
初级代码游戏1 个月前
C C++ 如何编写库级接口
c语言·c++·接口··编写库
@阿秋1 个月前
Netty入门基础:IO模型中BIO\NIO概念及区别【附演示代码】
netty
bin的技术小屋1 个月前
谈一谈 Netty 的内存管理 —— 且看 Netty 如何实现 Java 版的 Jemalloc(中)
java·后端·netty
永恒之月℡1 个月前
JavaWeb合集16-接口管理Swager
java·接口·接口测试·swager