Tomcat架构深度拆解:Connector和Container到底怎么配合的?

你每天都在用Tomcat------Spring Boot内嵌的就是它。但你真的了解Tomcat是怎么工作的吗?

当一个HTTP请求打到Tomcat,从Socket连接建立,到最终你的Servlet代码执行,中间到底经历了什么?Connector和Container这两个名字你肯定见过,但它们之间是怎么配合的?Engine、Host、Context、Wrapper这一层层的容器到底在做什么?这些问题的答案,决定了你做性能调优时是真懂还是瞎调。


一、Tomcat整体架构概览

先上一张Tomcat的整体架构脑图:

复制代码
Server(Tomcat实例,全局唯一)
  └── Service(对外提供完整服务,可多个)
        ├── Connector(连接器,负责接收请求,可多个)
        │     ├── HTTP/1.1 Connector(默认8.0后NIO)
        │     └── AJP Connector(与Web服务器集成,如nginx→tomcat)
        └── Engine(容器顶层,处理Connector交过来的请求)
              └── Host(虚拟主机,如localhost)
                    └── Context(一个Web应用)
                          └── Wrapper(一个Servlet的包装器)

这个结构的核心设计思想就一句话:连接和处理的分离

Connector只管"接收请求、解析协议、生成Request对象",它不懂业务。Container只管"按照容器层级逐层路由、执行过滤器链、最终调用Servlet",它不管请求是从哪个端口来的。

这种分离带来极大的灵活性。你可以配多个Connector------一个HTTP端口对外服务,一个AJP端口和Nginx对接,它们共享同一个Engine,请求最终都走到同一套Container处理逻辑里。

来看server.xml中最精简的配置结构:

java 复制代码
<Server port="8005" shutdown="SHUTDOWN">
  <Service name="Catalina">
    <!-- Connector:负责接客 -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    
    <!-- Engine:负责分活 -->
    <Engine name="Catalina" defaultHost="localhost">
      <Host name="localhost" appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <!-- Context通常不显式配置,由autoDeploy自动发现 -->
      </Host>
    </Engine>
  </Service>
</Server>

一个Server包着一个Service(通常就一个),Service里面左边挂Connector,右边挂Engine,Engine往下是Host→Context→Wrapper逐层嵌套。这就是Tomcat的骨架。

老梁说:很多人搞不清什么时候该配多个Service。答案是------当你需要完全隔离两组连接器和容器时。比如你有一个对外的Web服务和一个内部管理接口,希望它们用不同的线程池、不同的超时策略,那就拆成两个Service。但99%的场景,一个Service就够了,别给自己加复杂度。

二、Connector组件详解

Connector是Tomcat的"门面",它负责网络通信和协议解析。Connector内部有三层核心结构:Endpoint → Processor → Adapter

2.1 Endpoint:管Socket的那一层

Endpoint是真正的网络I/O层,它负责Socket的接收、读写和发送。Endpoint有两种实现:

  • NioEndpoint:基于Java NIO,从Tomcat 8.0开始成为默认
  • AprEndpoint:基于APR(Apache Portable Runtime),使用JNI调用本地库

NioEndpoint内部有几个关键角色,我用代码来说明:

java 复制代码
// 简化后的NioEndpoint核心结构
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel,SocketChannel> {
    
    // ① Acceptor线程:专门接收新连接
    private Acceptor<NioChannel> acceptor;
    
    // ② Poller线程:轮询已建立连接的IO事件
    private Poller[] pollers;
    private int pollerThreadCount = Math.min(2, Runtime.getRuntime().availableProcessors());
    
    // ③ Worker线程池:实际处理请求的业务线程
    private Executor executor; // 默认使用StandardThreadExecutor
}

这三个角色的协作方式是理解Tomcat并发模型的关键,后面讲请求旅程时会详细展开。

2.2 Processor:管协议解析的那一层

Processor负责将Endpoint拿到的字节流按照特定协议解析成Tomcat内部的Request对象。主要的Processor实现:

  • Http11Processor:解析HTTP/1.1协议
  • Http11NioProcessor (旧版)/ 现在统一为Http11Processor配合NioEndpoint
  • AjpProcessor:解析AJP协议

Processor解析完协议后,会生成一个CoyoteRequest对象(注意这不是最终的ServletRequest,后面Adapter会转换)。

2.3 Adapter:Connector和Container之间的桥梁

Adapter是Connector和Container之间唯一的粘合层。它的核心职责是:把CoyoteRequest/CoyoteResponse转换成ServletRequest/ServletResponse,然后交给Container去处理。

java 复制代码
// CoyoteAdapter的核心方法
public void service(org.apache.coyote.Request req, 
                     org.apache.coyote.Response res) {
    // 1. 将CoyoteRequest转换为HttpServletRequest
    Request request = connector.createRequest();
    request.setCoyoteRequest(req);
    
    // 2. 将CoyoteResponse转换为HttpServletResponse
    Response response = connector.createResponse();
    response.setCoyoteResponse(res);
    
    // 3. 关联request和response
    request.setResponse(response);
    
    // 4. 关键!交给Engine处理 ------ 从这里开始进入Container的世界
    connector.getService().getContainer().getPipeline()
            .getFirst().invoke(request, response);
}

注意第4步,这行代码就是Connector把请求"交接"给Container的关键点。getPipeline().getFirst().invoke()触发的是Pipeline-Valve责任链的第一个Valve,从此请求在Container层逐层流转。

2.4 BIO/NIO/APR三种IO模型的演进

Tomcat的IO模型经历了三个阶段,这是面试高频考点,但很多人只停留在"背答案"阶段:

模型 版本支持 原理 现状
BIO Tomcat 7前默认 一个连接占一个线程,阻塞式IO Tomcat 9已移除
NIO Tomcat 8后默认 基于Selector多路复用,Acceptor+Poller+线程池 主流方案
APR 需安装本地库 JNI调用APR,性能接近Native 大并发场景使用

BIO为什么被淘汰?核心原因是线程浪费 。BIO模式下,每个TCP连接都会占住一个工作线程,即使这个连接没有数据传输,线程也被阻塞在InputStream.read()上。1000个连接就要1000个线程,线程切换开销爆炸。

NIO用Selector多路复用解决了这个问题。Acceptor线程只管接收新连接,然后把Socket注册到Selector上;Poller线程轮询Selector,发现哪个Socket有数据可读,就把任务丢给工作线程池去处理。工作线程只在真正有数据可处理时才介入,大幅减少了空闲等待。

来看NioEndpoint中Poller的核心逻辑:

java 复制代码
// 简化后的Poller.run()方法
public void run() {
    while (true) {
        // 阻塞等待selector事件
        Iterator<SelectionKey> iterator = 
            selector.selectedKeys().iterator();
        while (iterator.hasNext()) {
            SelectionKey key = iterator.next();
            NioSocketWrapper socketWrapper = 
                (NioSocketWrapper) key.attachment();
            
            // 有读事件就绪,交给工作线程池处理
            if (key.isReadable()) {
                // 关键:把SocketWrapper丢给线程池
                getExecutor().execute(
                    new SocketProcessor(socketWrapper));
            }
        }
    }
}

这段代码揭示了NIO的精髓:Poller不处理业务,只负责"发现活",真正干活交给Worker线程池

BIO在Tomcat 9里已经彻底删掉了。但NIO和APR的选型,别盲目追APR。APR需要安装本地库(Linux上装apr和tomcat-native),部署复杂度上去了,且JVM层面的性能差距在现代JDK下已经很小。除非你是超高并发场景(比如单机上万QPS),否则NIO完全够用,部署简单,排查问题也方便。我见过好几个团队折腾APR配错了反而性能下降的,得不偿失。

三、Container组件详解

Container是Tomcat的"发动机",它负责处理Connector交过来的请求。Container有四层,从外到内依次是:Engine → Host → Context → Wrapper

3.1 四层容器的职责

容器 职责 例子
Engine 全局唯一,处理所有Connector交来的请求 Catalina引擎
Host 虚拟主机,根据Host头路由 localhost、www.example.com
Context 一个Web应用 /order-service
Wrapper 一个Servlet的包装器 OrderServlet

这个设计是典型的组合模式 ------每个容器都实现了Container接口,可以包含子容器。Engine包含多个Host,Host包含多个Context,Context包含多个Wrapper。

3.2 Pipeline-Valve责任链模式

每层Container内部都有一个Pipeline(管道),管道上挂着一串Valve(阀门)。请求到达某一层容器时,会先走过该层Pipeline上的Valve链,然后路由到下一层容器。

每个容器有一个Basic Valve (基础阀门),它是Pipeline链的最后一环,负责将请求路由到下一层容器。比如Engine的Basic Valve是StandardEngineValve,它的工作就是根据请求的Host头找到对应的Host,然后把请求丢过去。

我用一张文字流程图来展示完整的Pipeline-Valve链:

每一层的Basic Valve都干一件事:找到下一层该处理这个请求的子容器,然后把请求传过去。这就形成了从外到内逐层路由的效果。

Pipeline-Valve这个设计精髓在于可插拔。你可以在任意一层加自己的Valve,拦截请求做自定义处理。后面我会带你做一个自定义Valve的实验。但注意------Valve和Filter不是一回事。Valve是Tomcat层面的,作用在容器级别;Filter是Servlet规范层面的,作用在Wrapper内部。Valve比Filter更"靠前",连FilterChain都还没构建时就执行了。

四、一个HTTP请求在Tomcat中的完整旅程

理论讲完了,现在带你完整追踪一个HTTP请求从进入Tomcat到Servlet执行的全过程。这是这篇文章最核心的部分,请仔细看。

假设一个请求:GET http://localhost:8080/myapp/hello?name=laoliang

第一步:Acceptor接收TCP连接

Tomcat启动时,NioEndpoint会启动一个Acceptor线程(默认1个),它的工作就是一个死循环:不断调用ServerSocketChannel.accept()接收新的TCP连接。

java 复制代码
// Acceptor的核心循环(简化版)
protected void run() {
    while (!isStopped()) {
        // 接收新连接
        SocketChannel socketChannel = serverSock.accept();
        
        // 设置为非阻塞模式
        socketChannel.configureBlocking(false);
        
        // 注册到Poller的Selector上
        poller.register(socketChannel);
    }
}

Acceptor不管请求内容,它只负责"开门迎客"。连接建立后,立刻丢给Poller。

第二步:Poller轮询IO事件

Poller线程内部有一个Selector。Acceptor把SocketChannel注册到Poller的Selector上,关注OP_READ事件(有数据可读)。

java 复制代码
// Poller.register() ------ 将新连接注册到Selector
public void register(SocketChannel socketChannel) {
    NioSocketWrapper socketWrapper = new NioSocketWrapper(socketChannel);
    // 注册OP_READ事件
    socketChannel.register(selector, SelectionKey.OP_READ, socketWrapper);
}

Poller线程在另一个死循环中轮询Selector:

java 复制代码
// Poller核心循环(简化版)
public void run() {
    while (true) {
        selector.select(1000); // 阻塞最多1秒
        Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
        while (keys.hasNext()) {
            SelectionKey key = keys.next();
            if (key.isReadable()) {
                NioSocketWrapper socketWrapper = 
                    (NioSocketWrapper) key.attachment();
                // 取消读事件关注,避免重复触发
                key.cancel();
                // 关键:把任务提交到工作线程池
                getExecutor().execute(
                    new SocketProcessor(socketWrapper));
            }
        }
    }
}

Poller不读数据,不解析协议,只负责"发现哪些连接有数据了,然后丢给线程池"。这是NIO的核心优势------一个Poller线程可以管理上千个连接。

第三步:Worker线程池处理(SocketProcessor)

工作线程池拿到SocketProcessor后,开始真正处理请求:

java 复制代码
// SocketProcessor核心逻辑(简化)
protected void doRun() {
    // 1. 从Socket读取数据
    NioChannel channel = socketWrapper.getSocket();
    // 2. 交给Processor解析HTTP协议
    processor.process(channel); // → Http11Processor
    
    // Processor内部会调用Adapter
    // 3. CoyoteAdapter.service() 把请求交给Container
    adapter.service(coyoteRequest, coyoteResponse);
}

第四步:CoyoteAdapter转换并交给Container

java 复制代码
// CoyoteAdapter.service() (关键部分)
public void service(org.apache.coyote.Request req, 
                     org.apache.coyote.Response res) {
    Request request = connector.createRequest();
    request.setCoyoteRequest(req);
    Response response = connector.createResponse();
    response.setCoyoteResponse(res);
    
    // 关键!从Connector跨越到Container的这行代码
    connector.getService().getContainer()
            .getPipeline().getFirst().invoke(request, response);
}

从这行代码开始,请求正式进入Container的世界。

第五步:Engine的Pipeline-Valve链

Engine的Pipeline上,第一个Valve被调用。假设你没加自定义Valve,那第一个就是Basic Valve------StandardEngineValve

java 复制代码
// StandardEngineValve.invoke()
public void invoke(Request request, Response response) {
    Host host = (Host) request.getContext().getHost();
    host.getPipeline().getFirst().invoke(request, response);
}

它根据请求的Host头(localhost:8080中的localhost部分)找到对应的Host容器,把请求传过去。

第六步:Host的Pipeline-Valve链

java 复制代码
// StandardHostValve.invoke()
public void invoke(Request request, Response response) {
    Context context = request.getContext();
    context.getPipeline().getFirst().invoke(request, response);
}

根据URL路径/myapp/hello,找到myapp对应的Context容器。

第七步:Context的Pipeline-Valve链

java 复制代码
// StandardContextValve.invoke()
public void invoke(Request request, Response response) {
    Wrapper wrapper = request.getWrapper();
    wrapper.getPipeline().getFirst().invoke(request, response);
}

根据Servlet映射规则(/hello映射到HelloServlet),找到对应的Wrapper容器。

第八步:Wrapper的Pipeline-Valve链

Wrapper的Pipeline比较特殊,它的Basic Valve是StandardWrapperValve,这里会做两件关键的事:

java 复制代码
// StandardWrapperValve.invoke()(核心逻辑)
public void invoke(Request request, Response response) {
    // 1. 加载Servlet实例(首次请求会实例化)
    Servlet servlet = wrapper.allocate();
    
    // 2. 构造FilterChain(这是Servlet规范中的Filter链)
    ApplicationFilterChain filterChain = 
        new ApplicationFilterChain(request, response);
    filterChain.addFilter(/* 从web.xml中匹配的Filter */);
    
    // 3. 执行Filter链
    filterChain.doFilter(request, response);
    
    // Filter链的末尾会调用Servlet
    // → servlet.service(request, response);
    //   → doGet() 或 doPost() ...
}

到这里,请求终于到达了你写的HelloServlet.doGet()方法。

完整旅程图

用一张图总结整个请求流转:

理解这个旅程图有个实战价值------排查请求耗时问题。如果请求很慢,你得知道耗时在哪一段。是在Acceptor排队(说明连接积压,可能acceptCount太小)、Poller没及时处理(说明Poller线程数太少)、Worker线程池打满(说明maxThreads不够)、还是Servlet里业务逻辑慢?每一段的瓶颈,对应不同的调优手段。盲目调maxThreads解决不了所有问题。

五、关键配置解析

5.1 Connector核心配置

逐个解释:

参数 含义 默认值 调优建议
maxConnections 最大连接数(同时保持的TCP连接数) NIO:10000 根据内存评估
acceptCount 等待队列长度(连接数超过maxConnections后排队) 100 和maxConnections配合
maxThreads 工作线程池最大线程数 200 根据CPU密集/IO密集定
minSpareThreads 最小空闲线程数(预热) 10 低峰期保命
connectionTimeout 连接建立后的读超时 20000ms 慢客户端调小
keepAliveTimeout Keep-Alive空闲超时 connectionTimeout 影响连接复用
maxKeepAliveRequests 一个Keep-Alive连接最多处理请求数 100 防止连接泄漏

maxConnections vs acceptCount vs maxThreads 是最容易搞混的三个参数,我画个图说清楚:

简单说:

  • maxConnections管的是Poller能照看多少连接
  • acceptCount管的是满员后还能在门外排多少
  • maxThreads管的是同时有多少线程在干活

老梁说:这三个参数的配合是调优的核心。我见过最典型的坑是------线上突发流量,maxConnections和acceptCount都很大(看起来"容错率高"),但maxThreads只有200。结果就是大量请求在Worker线程池前排长队,connectionTimeout超时后客户端已经断开了,Tomcat还在傻等处理,白白浪费线程。正确思路是:maxThreads根据你的应用类型来定(CPU密集型不超过CPU核数×2,IO密集型可以200-500),acceptCount不要太大(等于maxThreads就够了,大了只是延迟报错而已),maxConnections根据内存和业务峰值来定。

5.2 Host核心配置

关键属性说明:

  • appBase:Web应用部署目录,Tomcat会自动扫描这个目录下的WAR包
  • unpackWARs:是否自动解压WAR包到磁盘(生产建议true,性能更好)
  • autoDeploy:运行时自动部署新应用(生产环境建议关闭,防止误操作)
  • reloadable:是否监控class文件变化自动重载(生产环境必须关闭,性能杀手)

reloadable="true"在开发时很好用,改了代码Tomcat自动重载。但在生产环境就是个坑------它后台会起线程定期扫描WEB-INF/classes目录的class文件变化,一旦有变化就重新加载整个Context。这个扫描本身就是性能开销,而且热重载容易导致内存泄漏(老ClassLoader卸载不干净)。生产环境请务必设为false。如果需要更新应用,走正规的重新部署流程。

六、实验:自定义一个Valve并加入Pipeline

理论理解了,来做个实验。我们写一个自定义Valve,记录每个请求的处理耗时,并把它加入Engine的Pipeline。

第一步:实现Valve接口

java 复制代码
package com.laoliang.tomcat.valve;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.apache.catalina.valves.ValveContext;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 自定义Valve:记录请求处理耗时
 * 挂在Engine层,所有请求都会经过
 */
public class TimingValve extends ValveBase {

    // 超过这个毫秒数就打WARN日志
    private int warnThreshold = 1000;

    @Override
    public void invoke(Request request, Response response, 
                      ValveContext valveContext) 
            throws IOException, ServletException {
        
        long startTime = System.currentTimeMillis();
        
        // 关键:调用getNext().invoke(),让请求继续往下走
        // 如果不调用,请求到此为止
        valveContext.invokeNext(request, response);
        
        long elapsed = System.currentTimeMillis() - startTime;
        String uri = request.getRequestURI();
        String method = request.getMethod();
        
        if (elapsed > warnThreshold) {
            System.out.println("[WARN] Slow request: " 
                + method + " " + uri + " took " + elapsed + "ms");
        } else {
            System.out.println("[INFO] " 
                + method + " " + uri + " - " + elapsed + "ms");
        }
    }

    public void setWarnThreshold(int warnThreshold) {
        this.warnThreshold = warnThreshold;
    }
}

第二步:在server.xml中配置

java 复制代码
<Engine name="Catalina" defaultHost="localhost">
  
  <!-- 自定义Valve,放在Engine层 -->
  <Valve className="com.laoliang.tomcat.valve.TimingValve"
         warnThreshold="500" />
  
  <Host name="localhost" appBase="webapps" ...>
    ...
  </Host>
</Engine>

第三步:在Spring Boot中配置(内嵌Tomcat)

Spring Boot不能直接改server.xml,但可以通过编程方式注册Valve:

java 复制代码
@Configuration
public class TomcatValveConfig {
    
    @Bean
    public TomcatServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory factory = 
            new TomcatServletWebServerFactory();
        
        factory.addContextCustomizers(context -> {
            // 注册到Context层
            context.addValve(new TimingValve());
        });
        
        // 如果要注册到Engine层,需要这样:
        factory.addEngineValves(new TimingValve());
        
        return factory;
    }
}

第四步:运行效果

启动后访问任意接口,控制台输出:

复制代码
[INFO] GET /api/users - 23ms
[INFO] POST /api/orders - 156ms
[WARN] Slow request: GET /api/reports/slow-query took 2300ms

这个Valve的好处是它比任何Filter都更"靠前"------在Servlet还没实例化之前就执行了,可以监控到所有请求,包括静态资源。

老梁说 :自定义Valve是个实用技巧。除了打耗时日志,我还用它做过:①请求体大小限制(在Filter层做的话,文件上传可能还没读完就限制了);②灰度路由------根据Header把请求路由到不同Context;③全链路追踪ID注入------在最早的地方注入traceId。记住Valve的核心调用是valveContext.invokeNext()漏掉这一行请求就断了,这是新手最常犯的错误。

七、Tomcat类加载机制简介

最后聊一个面试必考、但很多人理解不到位的话题------Tomcat的类加载机制。

7.1 为什么Tomcat要破坏双亲委派?

Java标准的双亲委派模型是:子ClassLoader先委托父ClassLoader加载,父加载不到才自己加载。但Tomcat有两个特殊需求:

  1. Web应用隔离:同一个Tomcat上部署了两个应用A和B,它们都依赖Spring 5和Spring 4的不同版本,必须做到类隔离
  2. WEB-INF/lib优先加载:Web应用自带的jar包要优先于Tomcat公共lib目录的同名jar包

这两个需求都要求破坏标准双亲委派。

7.2 Tomcat类加载器层次

7.3 WebappClassLoader的加载顺序

WebappClassLoader打破了双亲委派,它的加载顺序是:

复制代码
1. 检查本地缓存 → 已加载过?
2. 检查JVM缓存
3. 加载Java核心类(java.*、javax.*)→ 委托给System ClassLoader
   (这一步是为了安全,防止Web应用覆盖核心API)
4. 加载WEB-INF/classes目录的class文件 ← 优先!
5. 加载WEB-INF/lib目录的jar包 ← 优先!
6. 委托给Common ClassLoader加载Tomcat公共类

关键在第4、5步------Web应用自己的类和jar包优先于 Tomcat公共类加载。这就是为什么你放在WEB-INF/lib里的Spring版本会"覆盖"Catalina Home里lib目录的同名jar。

7.4 Spring Boot内嵌Tomcat的类加载

Spring Boot打成Fat Jar后,类加载机制和传统Tomcat有区别。Spring Boot用LaunchedURLClassLoader替代了WebappClassLoader,但核心逻辑类似------优先加载BOOT-INF/classes和BOOT-INF/lib。

来看一段验证代码:

java 复制代码
// 打印某个类的加载器
public class ClassLoaderTest {
    public static void main(String[] args) {
        // Spring的类
        ClassLoader springLoader = 
            org.springframework.context.ApplicationContext.class
                .getClassLoader();
        System.out.println("Spring class loaded by: " + springLoader);
        
        // JDK的类
        ClassLoader jdkLoader = 
            String.class.getClassLoader();
        System.out.println("JDK class loaded by: " + jdkLoader);
        
        // 自己的类
        ClassLoader myLoader = 
            ClassLoaderTest.class.getClassLoader();
        System.out.println("My class loaded by: " + myLoader);
    }
}

在Spring Boot应用中运行,你会看到:

  • Spring的类由LaunchedURLClassLoader加载
  • JDK核心类由Bootstrap加载(显示为null)
  • 你自己的类也由LaunchedURLClassLoader加载

类加载器的问题排查是运维的基本功。我最常遇到的是jar包冲突 ------明明WEB-INF/lib里有commons-lang3-3.12,运行时报NoSuchMethodError,一查发现Tomcat的lib目录里也有一个commons-lang3-3.1的旧版本被加载了。排查方法:用-verbose:class启动参数,或者在代码里System.out.println(StringUtils.class.getProtectionDomain().getCodeSource().getLocation())看类到底从哪个jar加载的。还有一个经典坑是SPI机制------JDBC驱动通过ServiceLoader加载,ServiceLoader用的是当前线程的ContextClassLoader,在Tomcat里这个ClassLoader和WebappClassLoader不一定一致,导致驱动找不到。遇到No suitable driver错误时往这个方向查。

理解了这个骨架,你做性能调优时就不会再瞎调。线程池满了、连接积压、请求慢------每个现象对应的是哪个环节的问题,你心里有了地图。

下一篇我们会深入Tomcat线程池调优,把maxConnections、acceptCount、maxThreads这三个参数的配合讲透,结合真实的压测数据来分析。90%的人在这三个参数上都会犯至少一个错误。

相关推荐
浪客川1 小时前
idea 技巧 region 的使用
java·ide·intellij-idea
没落英雄1 小时前
7. 从零开始搭建一个 AI Agent —— UI 卡片渲染系统
前端·人工智能·架构
万联WANFLOW1 小时前
月之暗面发布 Kimi K3:全球首个开源 3T 级大模型,前端编程竞技场登顶第一
网络·人工智能·架构·业界资讯
杉氧1 小时前
Ktor 全栈之路 (5):JWT 认证全流程实战 —— 打造安全通信闭环
android·架构·kotlin
CHANG_THE_WORLD1 小时前
逐层拆解:C++ 虚函数从对象内存到手工调用的完整过程
java·开发语言·c++
我登哥MVP1 小时前
走进 Gang of Four 设计模式:过滤器模式
java·设计模式·过滤器模式
xcLeigh2 小时前
从搜索引擎到 AI 编程:开发者获取知识方式的范式转移
人工智能·ai·架构·ai编程·开发·范式转移
吃饱了得干活2 小时前
实时弹幕应该怎么搞?一篇带你从入门到生产级实战
java·后端
XUHUOJUN2 小时前
Azure Stack Hub 套餐、计划与订阅模型深度解析(下篇)
microsoft·架构·azure stack