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

news2024/9/21 18:46:09

背景

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2140213.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在k8s中,客户端访问服务的链路流程,ingress--->service--->deployment--->pod--->container

ingress是一个API资源。 其核心作用是nginx网页服务器。 当客户端访问服务器不同的url时, 用不同的location提供服务。 在k8s之外&#xff0c;nginx的配置一般如下&#xff1a; http {server {listen 80;server_name localhost;location / {root html; …

文件的应用实例

目录 1、拷贝文件 2、遍历文件夹 1、拷贝文件 说明&#xff1a;将一张图片/一首歌拷贝到另外一个目录下&#xff0c;要求使用read()和write()原生方法完成 """思路分析&#xff1a;1、打开源文件(需要拷贝的文件)&#xff0c;读取源文件的数据2、打开目标文…

网络安全学习(四)渗透工具msf

本文简要介绍metasploit framework&#xff0c;是一款渗透工具。官网地址&#xff1a;Metasploit | Penetration Testing Software, Pen Testing Security | Metasploit msf是一个框架&#xff0c;可以加载各种模块&#xff0c;这是它的最强大之处。 kali中有此工具。 点击即…

python中的各类比较与计算

运算符 1.算数运算符2.关系运算符3.逻辑运算符4.关于短路求值5.赋值运算符1&#xff09;的使用链式赋值多元赋值 2)复合赋值运算符 6.位运算符7.成员运算符8.身份运算符 1.算数运算符 # 加 print(1 2) # 减 print(2 - 1) # 乘 print(1 * 2) # 余数 4%31余数为1 print(4 % 3…

C++第五十一弹---IO流实战:高效文件读写与格式化输出

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 目录 1. C语言的输入与输出 2. 流是什么 3. CIO流 3.1 C标准IO流 3.2 C文件IO流 3.2.1 以写方式打开文件 3.2.1 以读方式打开文件 4 stringstre…

【测试方案】软件测试管理规程(doc源文件)

软件测试规程的作用在于确保软件测试活动的系统性、规范性和一致性。它明确了测试的目标、范围、方法、流程以及所需资源&#xff0c;为测试人员提供了明确的指导和操作规范。通过遵循测试规程&#xff0c;可以提高测试效率&#xff0c;减少测试遗漏和错误&#xff0c;保证软件…

NC 表达式求值

系列文章目录 文章目录 系列文章目录前言 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂了就去分享给你的码吧。 描述 请写一个整数…

MySQL8.0.13-函数索引

目录 什么是函数索引 语法 函数索引测试 创建表结构 插入数据 创建普通索引 查看执行计划 创建函数索引 查看执行计划 查看索引信息 函数索引效率 普通索引 函数索引 分析 注意事项 老版本如何实现函数索引 什么是函数索引 本篇主要介绍 MySQL 的函数索引&…

【数据结构】顺序表和链表经典题目

系列文章目录 单链表 动态顺序表实现通讯录 顺序表 文章目录 系列文章目录前言一、顺序表经典例题1. 移除元素2. 合并两个有序数组 二、链表经典例题1. 移除链表元素2. 反转链表3. 合并两个有序链表4. 链表的中间节点5. 环形链表的约瑟夫问题 总结 前言 我们通过前面对顺序表…

NVM(node.js版本工具)的使用

1.nvm是什么 NVM 是 Node Version Manager 的缩写&#xff0c;它是一个用于管理 Node.js 版本的命令行工具。通过NVM&#xff0c;你可以在同一台机器上安装和切换多个 Node.js 版本&#xff0c;对于开发和测试在不同 Node.js 版本上运行的应用程序非常有用。 2.下载 下载之前…

『功能项目』眩晕图标显示【52】

我们打开上一篇51调整Boss技能bug的项目&#xff0c; 本章要做的事情是在释放法师的眩晕技能时&#xff0c;boss01处在眩晕动画时显示一个眩晕图标 首先双击Boss01预制体进入预制体空间 创建一个Image重命名为StateUIdiz 代表第一个受击状态 设置Canavas 并且修改Canvas的渲染…

Java 学习全攻略:从入门到精通的详细指南

目录 一、引言 Java 的背景和发展 学习 Java 的意义 二、Java 的核心特性 1. 面向对象编程&#xff08;OOP&#xff09; 2. 跨平台性 3. 自动内存管理 4. 强大的标准库 三、Java 基础语法 1. 变量和数据类型 原始数据类型 引用数据类型 2. 运算符 3. 控制结构 条…

柳淘鸿黄金沁透发热面膜:肌肤逆龄之旅的秘密武器!

柳淘鸿黄金沁透发热面膜&#xff1a;肌肤逆龄之旅的秘密武器&#xff01;"柳淘鸿的黄金沁透发热面膜液融合了中国发明专利,专利号:ZL202310228041.5对应成分:胶原, 金&#xff0c;珍珠粉以及多种珍贵植物萃取精华&#xff0c;是肌肤逆龄之旅的绝密武器。这款面膜液温和滋养…

Git之误执行git rm -r解决方案(六十七)

简介&#xff1a; CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布&#xff1a;《Android系统多媒体进阶实战》&#x1f680; 优质专栏&#xff1a; Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a; 多媒体系统工程师系列【…

视频分割操作教程

1、打开剪映 2、点击开始创作上面的“”&#xff0c;选择视频&#xff0c;点击添加按钮&#xff0c;导入一个视频素材到剪映 3、滑动视频&#xff0c;让视频竖线到合适位置 4、点击视频&#xff0c;出现白色边框 5、点击工具栏“分割”&#xff0c;然后点击需要删除的视频部分 …

03 战略的本质与实践 - 战略管理实践的启示

1,战略有一定复杂性。在学术界就有很多学派。明斯伯格,加拿大的管理学家,认为有10大学派。 我看来有三个方面: 理性学派:通过规划、主动管理 演进学派:根据一个不确定性的环境,自发自下而上来形成 过程管理:重要的不是结果,而是过程 简单的看,如下图。 显示公司自己的…

查询分类数据序列中的每个类别 Series.cat.categories

【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 1.定义分类序列s 2.查询s中的各类别 Series.cat.categories 选择题 关于以下代码输出结果的说法中正确的是? import pandas as pd s pd.Series([a,b,a,b], dtypecategory) print("【…

windows系统安装docker

参考&#xff1a;GitHub - tech-shrimp/docker_installer: Docker官方安装包&#xff0c;用来解决因国内网络无法安装使用Docker的问题 1.windows系统安装docker cmd 右键 以管理员身份运行 输入 wsl --set-default-version 2 wsl --update --web-download GitHub - tech-s…

系统架构设计师教程 第5章 5.2 需求工程 笔记

5.2 需求工程 ★★★★★ 软件需求是指用户对系统在功能、行为、性能、设计约束等方面的期望。 软件需求包括3个不同的层次&#xff1a;业务需求、用户需求和功能需求(也包括非功能需求)。 (1)业务需求 (business requirement) 反映了组织机构或客户对系统、产品高层次的目标…

Flutter启动无法运行热重载

当出现这种报错时&#xff0c;大概率是flutter的NO_Proxy出问题。 请忽略上面的Android报错因为我做的是windows开发这个也就不管了哈&#xff0c;解决下面也有解决报错的命令大家执行一下就行。 着重说一下Proxy的问题&#xff0c; 我们看到提示NO_PROXY 没有设置。 这个时候我…