Tomcat 8.5 源码分析

news2025/1/3 11:15:17

一、获取源码并启动程序

获取教程地址

总体架构

在这里插入图片描述

二、Tomcat的启动入口

Catalina类主要负责 具体的管理类,而Bootstrap类是启动的入口(main方法)。

/**
     * Main method and entry point when starting Tomcat via the provided
     * scripts.
     *
     * @param args Command line arguments to be processed
     */
    public static void main(String args[]) {

        synchronized (daemonLock) {
            if (daemon == null) {
                // Don't set daemon until init() has completed
                Bootstrap bootstrap = new Bootstrap();
                try {
                    bootstrap.init();
                } catch (Throwable t) {
                    handleThrowable(t);
                    t.printStackTrace();
                    return;
                }
                daemon = bootstrap;
            } else {
                // When running as a service the call to stop will be on a new
                // thread so make sure the correct class loader is used to
                // prevent a range of class not found exceptions.
                Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
            }
        }

        try {
            String command = "start";
            if (args.length > 0) {
                command = args[args.length - 1];
            }

            if (command.equals("startd")) {
                args[args.length - 1] = "start";
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stopd")) {
                args[args.length - 1] = "stop";
                daemon.stop();
            } else if (command.equals("start")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
            } else if (command.equals("stop")) {
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
                daemon.load(args);
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
                System.exit(0);
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable t) {
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }
    }

2.1 Bootstrap#init()

	/**
     * Initialize daemon.
     * @throws Exception Fatal initialization error
     */
    public void init() throws Exception {
        //1、初始化类加载器
        initClassLoaders();
        //2、设置当前线程的上下文加载器为catalinaLoader      
        Thread.currentThread().setContextClassLoader(catalinaLoader);
        //3、预先加载tomcat、javax包的自定义类
        SecurityClassLoad.securityClassLoad(catalinaLoader);
        // 加载启动类以及调用setParentClassLoad()方法      if (log.isDebugEnabled())
        if (log.isDebugEnabled()) {
            log.debug("Loading startup class");
        }
        //4.通过反射创建Catalina对象
        Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.getConstructor().newInstance();
        // Set the shared extensions class loader
        if (log.isDebugEnabled()) {
            log.debug("Setting startup class properties");
        }
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);
        //5.将startupInstance对象赋值给catalinaDaemon
        catalinaDaemon = startupInstance;
    }

关键步骤

  1. 初始化类加载器
  2. 设置当前线程的上下文加载器为catalinaLoader
  3. 预先加载tomcat、javax包的自定义类
  4. 通过反射创建Catalina对象
  5. 将startupInstance对象赋值给catalinaDaemon

在这里插入图片描述

2.2 Bootstrap#init()#initClassLoaders()

init()初始化的方法也相对的简单,首先调用initClassLoaders()初始化类加载器,使得tomcat可以加载应用程序类,接着设置当前线程的上下文加载器为CatalinaLoader。

 private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if (commonLoader == null) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader = this.getClass().getClassLoader();
            }
            //在initClassLoaders()初始化方法中可发现会创建三种类加载器并赋予成员变量,其中catalinaLoader与sharedLoader加载器的父加载器都是commonLoader。
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }

在initClassLoaders()初始化方法中可发现会创建三种类加载器并赋予成员变量,其中catalinaLoader与sharedLoader加载器的父加载器都是commonLoader。

2.3 Catalina#load()

load()方法主要完成StardardServer及子组件的初始化,下图是该方法的主要序列流程:

 public void load() {
        if (loaded) {
            return;
        }
        loaded = true;
        long t1 = System.nanoTime();
        initDirs();
        // Before digester - it may be needed
        initNaming();
        // Create and execute our Digester
        Digester digester = createStartDigester();
        InputSource inputSource = null;
        InputStream inputStream = null;
        File file = null;
        try {
            try {
                file = configFile();
                //读取serve.xml文件
                inputStream = new FileInputStream(file);
                inputSource = new InputSource(file.toURI().toURL().toString());
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("catalina.configFail", file), e);
                }
            } 
            try {
                inputSource.setByteStream(inputStream);
                digester.push(this);
                //解析serve.xml文件
                digester.parse(inputSource);
            } catch (SAXParseException spe) {
                log.warn("Catalina.start using " + getConfigFile() + ": " +
                        spe.getMessage());
                return;
            } catch (Exception e) {
                log.warn("Catalina.start using " + getConfigFile() + ": " , e);
                return;
            }
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // Ignore
                }
            }
        }
        getServer().setCatalina(this);
        getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
        getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
        // Stream redirection
        initStreams();
        // Start the new server
        try {
            getServer().init();
        } catch (LifecycleException e) {
            if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
                throw new java.lang.Error(e);
            } else {
                log.error("Catalina.start", e);
            }
        }
    }

在这里插入图片描述

2.4 standerServer#initInternal()

在执行LifecycleBase#initInternal()抽象方法时,将由standerServer#initInternal()的方法完成初始化。

 @Override
    protected void initInternal() throws LifecycleException {
        //1、调用LifecycleMBeanBase#initInternal方法
        super.initInternal();

       //2、注册全局字符串缓存,如果有多个Server则也会注册多个字符串缓存对象
        onameStringCache = register(new StringCache(), "type=StringCache");

        //3、注册MBeanFactory
        MBeanFactory factory = new MBeanFactory();
        factory.setContainer(this);
        onameMBeanFactory = register(factory, "type=MBeanFactory");

        //4、注册全局命名资源并初始化,该全局命名资源是配置在server.xml中
        globalNamingResources.init();

       //5、使用catalina的父加载器校验系统JAR包是否包含MANIFEST文件等,此处忽略部分代码
      
        //6、初始化自定义的service
        for (Service service : services) {
            service.init();
        }
    }


而service组件的init()方法主要做了以下事情:

  1. 初始化StandardEngine容器。
    • 创建指定或默认的Realm。
  2. 初始化绑定的StandardThreadExecutor线程执行器。
  3. 初始化绑定的MapperListener监听器。
  4. 更新mapper监听器的生命周期状态为LifecycleState.STARTING。
  5. 查找并绑定默认的虚拟主机Host。
  6. 绑定该监听器到Engine及子容器。
  7. 注册Engine容器到绑定的虚拟主机Host以及上下文Context、Wrapper。
  8. 初始化绑定的所有connector连接器。
    /**
     * Invoke a pre-startup initialization. This is used to allow connectors to bind to restricted ports under Unix
     * operating environments.
     */
    @Override
    protected void initInternal() throws LifecycleException {
        super.initInternal();
        if (engine != null) {
            engine.init();
        }
        // Initialize any Executors
        for (Executor executor : findExecutors()) {
            if (executor instanceof JmxEnabled) {
                ((JmxEnabled) executor).setDomain(getDomain());
            }
            executor.init();
        }
        // Initialize mapper listener
        mapperListener.init();
        // Initialize our defined Connectors
        synchronized (connectorsLock) {
        	//初始化所有关联的connector连接器
            for (Connector connector : connectors) {
                try {
                    connector.init();
                } catch (Exception e) {
                    String message = sm.getString("standardService.connector.initFailed", connector);
                    log.error(message, e);

                    if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
                        throw new LifecycleException(message);
                    }
                }
            }
        }
    }

在整个service初始化过程中,connector连接器的初始化尤其重要,以下为connector连接器初始化代码:

  1. 创建、初始化Coyote适配器,并绑定APR或HTTP1.1协议处理器。
  2. 检查parseBodyMethodsSet是否有值,若没则设置为POST方法。
  3. 判断APR的native库是否必须且协议处理器实例是否已创建,若都不满足则抛出异常。
  4. 判断APR本地库是否必须且APR协议处理器是否可用,若都不满足则抛出异常。
  5. 判断APR协议处理器是否可用且Apr协议处理器是否使用OpenSSL且协议处理器是否AbstractHttp11JsseProtocol类型。
    • 判断是否启用SSL且其类名为空,若满足则使用OpenSSLImplementation类名(OpenSSL与JSSE的配置兼容)。
    • 否则执行步骤6
  6. 初始化协议处理器(该协议处理器实例时在server.xml解析时创建的)。
@Override
protected void initInternal() throws LifecycleException {
    super.initInternal();
    // 1、创建并初始化Coyote适配器,并关联至协议处理器中
    adapter = new CoyoteAdapter(this);
    protocolHandler.setAdapter(adapter);

    //2、确认parseBodyMethodsSet有默认值,parsetBodyMethods默认值为POST方法
    if (null == parseBodyMethodsSet) {
        setParseBodyMethods(getParseBodyMethods());
    }
  //3、判断是否需要APR本地库 AND 判断Apr协议处理器实例是否被创建,若是Apr本地路且没创建Apr协议处理器则抛出异常
    if (protocolHandler.isAprRequired() && !AprLifecycleListener.isInstanceCreated()) {
        throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoAprListener",
                getProtocolHandlerClassName()));
    }
   //4、判断是否需要APR/native库 AND Apr协议处理器是否可用,若是需要Apr本地库且没有Apr协议处理器可用则抛出异常
    if (protocolHandler.isAprRequired() && !AprLifecycleListener.isAprAvailable()) {
        throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoAprLibrary",
                getProtocolHandlerClassName()));
    }
   //5、判断Apr协议处理器是否可用 AND Apr协议处理器是否使用OpenSSL AND 协议处理器是否AbstractHttp11JsseProtocol类型
    if (AprLifecycleListener.isAprAvailable() && AprLifecycleListener.getUseOpenSSL() &&
            protocolHandler instanceof AbstractHttp11JsseProtocol) {
        AbstractHttp11JsseProtocol<?> jsseProtocolHandler =
                (AbstractHttp11JsseProtocol<?>) protocolHandler;
       //5.1、如果SSL启用了且SSL实现类名为空,则使用OpenSSLImplementation类名
        if (jsseProtocolHandler.isSSLEnabled() &&
                jsseProtocolHandler.getSslImplementationName() == null) {
            // 如果APR可用,可以使用OpenSSL,因为OpenSSL与JSSE的配置兼容。
            jsseProtocolHandler.setSslImplementationName(OpenSSLImplementation.class.getName());
        }
    }

    try {
       //协议处理器执行初始化(在server.xml解析时创建protocolHandler)
        protocolHandler.init();
    } catch (Exception e) {
        throw new LifecycleException(
                sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
    }
}

2.5Catalina#start()

在这里插入图片描述

public void start(){
     //判断server是否为空,为空则执行初始化
    if (getServer() == null) {
        load();
    }
    //若server为空,说明未能正确配置server.xml或者server初始化时异常
    if (getServer() == null) {
        log.fatal("Cannot start server. Server instance is not configured.");
        return;
    }
    //此处省略部分代码
  
    try {
        getServer().start();
    } catch (LifecycleException e) {
        log.fatal(sm.getString("catalina.serverStartFail"), e);
        try {
            getServer().destroy();
        } catch (LifecycleException e1) {
            log.debug("destroy() failed for failed Server ", e1);
        }
        return;
    }

     //此处忽略部分代码

    // 注册关闭钩子
    if (useShutdownHook) {
        if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);

        // If JULI is being used, disable JULI's shutdown hook since
        // shutdown hooks run in parallel and log messages may be lost
        // if JULI's hook completes before the CatalinaShutdownHook()
        LogManager logManager = LogManager.getLogManager();
        if (logManager instanceof ClassLoaderLogManager) {
            ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                    false);
        }
    }

    if (await) {
       //创建ServerSocker并监听端口
        await();
       //停止standardServer实例
        stop();
    }
}

上面主要是启动tomcat中各个组件、容器,这时候还需要创建等待关闭tomcat的ServerSocket以及它监听的端口,而await()方法的作用正是创建ServerSocket并等待接收关闭命令的。

@Override
public void await(){
   //该处省略部分代码...
  
    //创建ServerSocker并监听端口
    try {
        awaitSocket = new ServerSocket(port, 1,InetAddress.getByName(address));
    } catch (IOException e) {
        log.error("StandardServer.await: create[" + address+ ":" + port+ "]: ", e);
        return;
    }

    try {
        awaitThread = Thread.currentThread();
        // 循环等待有效连接和命令
        while (!stopAwait) {
            ServerSocket serverSocket = awaitSocket;
            if (serverSocket == null) {
                break;
            }
            // 等待下一个连接
            Socket socket = null;
            StringBuilder command = new StringBuilder();
            try {
                InputStream stream;
                long acceptStartTime = System.currentTimeMillis();
                try {
                    socket = serverSocket.accept();
                    socket.setSoTimeout(10 * 1000);  // Ten seconds
                    stream = socket.getInputStream();
                } catch (SocketTimeoutException ste) {
                    log.warn(sm.getString("standardServer.accept.timeout",
                            Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste);
                    continue;
                } catch (AccessControlException ace) {
                    log.warn(sm.getString("standardServer.accept.security"), ace);
                    continue;
                } catch (IOException e) {
                    if (stopAwait) {
                        break;
                    }
                    log.error(sm.getString("standardServer.accept.error"), e);
                    break;
                }

                // 从套接字读取一组字符
                int expected = 1024; // Cut off to avoid DoS attack
                while (expected < shutdown.length()) {
                    if (random == null)
                        random = new Random();
                    expected += (random.nextInt() % 1024);
                }
                while (expected > 0) {
                    int ch = -1;
                    try {
                        ch = stream.read();
                    } catch (IOException e) {
                        log.warn(sm.getString("standardServer.accept.readError"), e);
                        ch = -1;
                    }
                    // 若字符是控制字符或者EOF(-1)则终止循环
                    if (ch < 32 || ch == 127) {
                        break;
                    }
                    command.append((char) ch);
                    expected--;
                }
            } finally {
                // 完成操作后关闭socket
                try {
                    if (socket != null) {
                        socket.close();
                    }
                } catch (IOException e) {
                    // Ignore
                }
            }

            // 判断命令内容是否SHUTDOWN
            boolean match = command.toString().equals(shutdown);
            if (match) {
                log.info(sm.getString("standardServer.shutdownViaPort"));
                break;
            } else
                log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString()));
        }
    } finally {
        ServerSocket serverSocket = awaitSocket;
        awaitThread = null;
        awaitSocket = null;
        // 关闭ServerScoket
        if (serverSocket != null) {
            try {
                serverSocket.close();
            } catch (IOException e) {
              
            }
        }
    }
}

结束

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

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

相关文章

C# Post 接口请求样例

很久没有写接口了&#xff0c;正好今天写到了接口&#xff0c;记录一下 封装Http Post请求&#xff0c;这里的请求头是 request.ContentType “application/json”; 复制后根据实际去修改&#xff0c;不要生搬硬套搞拿来主义&#xff1a; /// <summary>/// 发送http请求…

Spring Cloud 远程接口调用OpenFeign负载均衡实现原理详解

环境&#xff1a;Spring Cloud 2021.0.7 Spring Boot 2.7.12 配置依赖 maven依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency&…

什么是XSS攻击及其类型和危害

什么是XSS攻击及其类型和危害 跨站脚本攻击&#xff08;Cross-Site Scripting&#xff0c;简称 XSS&#xff09;是一种常见的网络安全漏洞&#xff0c;攻击者通过在受害者的浏览器中注入恶意脚本&#xff0c;从而在受害者的浏览器端执行恶意代码&#xff0c;从而实现攻击目的。…

在CSDN博客平台上吸引铁粉诀窍

&#x1f3c6;荣誉认证&#xff1a;51CTO博客专家博主、TOP红人、明日之星&#xff1b;阿里云开发者社区专家博主、技术博主、星级博主。 ⭐本文介绍⭐ 在社交媒体时代&#xff0c;拥有一批忠实的粉丝对于博主来说非常重要。这些铁粉不仅能够给予支持和鼓励&#xff0c;还能为…

第十八章 番外篇:混合精度训练

参考教程&#xff1a; https://pytorch.org/tutorials/recipes/recipes/amp_recipe.html?highlightamp https://pytorch.org/docs/stable/amp.html https://arxiv.org/pdf/1710.03740.pdf https://zhuanlan.zhihu.com/p/79887894 文章目录 原理float 32float 16混合精度 代码实…

cmake编译mingw下使用的zlib

目录 一、准备 二、cmake构建 三、make编译 一、准备 zlib Home Site zlib1.2.11&#xff08;2017.2.15&#xff09; 二、cmake构建 有cmakeLists.txt&#xff0c;直接用cmake进行构建 然后点击generate&#xff0c;接下来只能用命令行编译&#xff0c;在build目录执行…

选购螺杆支撑座要考虑哪些因素?

为了可以保证螺杆支撑座的使用效果&#xff0c;同时也能够发挥出更好的使用功能&#xff0c;避免出现各种质量隐患&#xff0c;建议大家在购买的时候一定要在专业正规的厂家进行选购&#xff0c;那么&#xff0c;我们在选购的时候要考虑哪些方面的因素呢&#xff1f; 1、考虑到…

曲柄滑块运动学求解基于Matlab

参考文档&#xff1a; 曲柄滑块机构运动分析..doc-原创力文档 偏置曲柄滑块机构的运动学分析 - 豆丁网 偏置式曲柄滑块机构仿真与运动分析 - 豆丁网 https://www.cnblogs.com/aksoam/p/17013811.html function main %输入已知数据 close all clear; i1100; i2300; e56; hd …

统一异常处理,自定义异常

目录 一、制造异常 Swagger中测试 二、统一异常处理 1、创建统一异常处理器 2、测试 三、处理特定异常 1、添加依赖 2、添加异常处理方法 3、测试 4、恢复制造的异常 四、自定义异常 1、创建自定义异常类 2、添加异常处理方法 3、修改Controller 4、测试 返回异…

3.1 Bootstrap 字体图标(Glyphicons)

文章目录 Bootstrap 字体图标(Glyphicons)什么是字体图标&#xff1f;获取字体图标CSS 规则解释带有导航栏的字体图标定制字体图标定制字体尺寸定制字体颜色应用文本阴影 Bootstrap 字体图标(Glyphicons) 本章将讲解字体图标(Glyphicons)&#xff0c;并通过一些实例了解它的使用…

【SpringBoot】SpringBoot的自动配置源码解析

文章目录 1. SpringBoot的自动配置概念2. SpringBoot自动配置的原理3. EnableAutoConfiguration4. 常用的Conditional注解 1. SpringBoot的自动配置概念 SpringBoot相对于SSM来说&#xff0c;主要的优点就是简化了配置&#xff0c;不再需要像SSM哪有写一堆的XML配置&#xff0…

SQLSERVER的truncate和delete有区别吗?

一&#xff1a;背景 1. 讲故事 在面试中我相信有很多朋友会被问到 truncate 和 delete 有什么区别 &#xff0c;这是一个很有意思的话题&#xff0c;本篇我就试着来回答一下&#xff0c;如果下次大家遇到这类问题&#xff0c;我的答案应该可以帮你成功度过吧。 二&#xff1…

全网最细,Pytest自动化框架fixture和conftest.py实战详解(细致)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 fixture说明 fix…

你一定不知道的自动化测试的9大规则

目录 前言 应该去做的事情 雇用合适的人 在寻找正确的测试自动化工具方面花点时间 轻装上阵 让开发人员参与到自动化过程中来 在ci/cd上投资时间 不应该做的事情 不要因为一个工具被追捧就选择它 不要试图将一切都自动化 不要太早实现自动化 永远不要用自动化来取代…

C语言-报错集锦-02-munmap_chunk(): invalid pointer: 0x0000000001d2e150 ***

一、报错信息 [2023-7]--[ Debug ]--Destroy DqlResult Struct OK [2023-7]--[ Debug ]--Destroy Moia Base Job : OK [2023-7]--[ Debug ]--Destroy Moia Base Job : OK [2023-7]--[ Debug ]--Destroy Moia Base Job : OK [2023-7]--[ Debug ]--Destroy Mo…

Redis学习(二)线程安全、分布式锁、消息队列

文章目录 优惠券秒杀全局ID生成器优惠券秒杀下单超卖问题一人一单 分布式锁基于Redis的setnx指令实现分布式锁解决锁误删问题基于Lua脚本实现多条指令原子性Redis调用Lua脚本Java中使用Lua脚本 RedissonRedisson快速入门Redisson可重入锁原理Redisson的锁重试和Watchdog机制Red…

【经济调度】基于多目标宇宙优化算法优化人工神经网络环境经济调度研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

适配各类大模型应用!手把手教你选择 Zilliz Cloud 实例类型

作为大模型时代备受关注的细分赛道&#xff0c;向量数据库可以不仅为大模型提供存储和向量检索的功能&#xff0c;还能适配各种 AI 应用场景&#xff0c;例如聊天机器人、内容审核、增强 LLM 知识库等。 不过&#xff0c;对于向量数据库的开发者而言&#xff0c;成本是绕不开的…

Spring学习笔记---SpringBoot快速入门

Spring学习笔记---SpringBoot快速入门 Spring学习笔记---SpringBoot1 SpringBoot简介1.1 SpringBoot快速入门1.1.1 开发步骤1.1.1.1 创建新模块1.1.1.2 创建 Controller1.1.1.3 启动服务器1.1.1.4 进行测试 1.1.2 对比1.1.3 官网构建工程1.1.3.1 进入SpringBoot官网1.1.3.2 选择…

(二)springboot实战——springboot基于多端内容协商适配实现json、xml、yaml等格式数据统一返回

前言 在实际应用开发场景中&#xff0c;我们有需求实现多端内容请求的适配&#xff0c;例如某些客户端需要返回json数据&#xff0c;有些客户端需要返回xml数据&#xff0c;有些客户端要返回yaml数据&#xff0c;这个时候就需要服务端做内容返回的适配&#xff0c;如果按照提供…