2026年IT就业市场:开发者应投资哪些技能

1. 概览

IT就业市场正朝着对每位开发者提出更多要求的方向发展。仅能写出功能特性是不够的。你还需要理解这个功能在生产环境中如何运行、如何故障以及需要花费多少成本。

本文不提供薪资数据或预测。相反,本文分析了五个基础技能组,这些技能组目前受到关注,解释了原因,并通过Java 21/Spring Boot 3.x代码示例说明每个技能组。读完后,你将知道应该优先学习什么、如何立即应用到项目中,以及什么时候不应该盲目跟风。

2. 为什么技能要求在转变

有三个主要驱动力,你可以在大多数招聘信息中观察到它们:

  • AI编码辅助工具已经普及。 “样板”代码生成得更快了。开发者的价值转向了设计、审查、测试和对结果的责任。
  • 企业注重运维成本。 一个资源使用量较少且更容易诊断错误的服务会直接节省成本。
  • 团队更小,所有权更广。 “你构建它,你运维它”模式让开发者需要了解打包、监控和事故处理。

由此,以下五个技能组成为长期高收益投资的候选。

3. 以受控的方式将AI集成到应用中

目前最常提到的技能不是“会用chatbot”。而是能够将LLM引入真实系统的同时保持稳定性、可测性和备选方案。

核心原则:将LLM视为一个不可信的外部依赖。它的输出可能格式错误、缓慢或网络错误。

3.1. 在接口后面隐藏LLM

@FunctionalInterface
public interface LlmClient {
    String complete(String prompt);
}

public enum TicketCategory { BILLING, TECHNICAL, OTHER }

public class TicketClassifier {

    private final LlmClient llm;

    public TicketClassifier(LlmClient llm) {
        this.llm = llm;
    }

    public TicketCategory classify(String message) {
        String prompt = """
            将以下支持请求分类到单一标签中:BILLING、TECHNICAL、OTHER。
            只返回标签,不要解释。
            请求:%s
            """.formatted(message);
        try {
            String raw = llm.complete(prompt).trim().toUpperCase();
            return TicketCategory.valueOf(raw);
        } catch (RuntimeException e) {
            return TicketCategory.OTHER;
        }
    }
}

解释:

  • LlmClient是你自己的接口。真实实现可以调用任何提供商的HTTP接口或使用像Spring AI这样的库。业务逻辑不依赖于特定的提供商。
  • Prompt要求输出是唯一的标签。格式限制帮助你解析结果。
  • TicketCategory.valueOf(raw)同时进行解析和验证。如果模型返回长句子,IllegalArgumentException被捕获,系统回退到OTHER。网络错误(通常是RuntimeException)也进入这个分支。

3.2. 不需要调用真实模型就进行测试

由于LlmClient是functional interface,你可以传入lambda作为fake:

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;

class TicketClassifierTest {

    @Test
    void givenValidLabel_whenClassify_thenReturnCategory() {
        TicketClassifier classifier = new TicketClassifier(prompt -> " billing\n");

        assertThat(classifier.classify("我被扣了两次款"))
            .isEqualTo(TicketCategory.BILLING);
    }

    @Test
    void givenUnexpectedOutput_whenClassify_thenFallbackToOther() {
        TicketClassifier classifier = new TicketClassifier(prompt -> "可能是 BILLING");

        assertThat(classifier.classify("我被扣了两次款"))
            .isEqualTo(TicketCategory.OTHER);
    }

    @Test
    void givenClientFailure_whenClassify_thenFallbackToOther() {
        TicketClassifier classifier = new TicketClassifier(prompt -> {
            throw new IllegalStateException("timeout");
        });

        assertThat(classifier.classify("应用崩溃了")).isEqualTo(TicketCategory.OTHER);
    }
}

第一个测试展示了代码能处理空格和小写。后两个测试证明系统在模型“说太多”或连接失败时不会崩溃。这正是招聘者想看到的:你知道LLM会出错,并且为此设计了系统。

注意: 不应该用LLM做那些用规则、正则表达式或SQL就能精确且便宜地完成的工作。也避免用LLM做那些需要确定结果并可以审计的决策,比如计费或权限分配。

4. 使用虚拟线程进行现代并发

大多数后端是I/O密集型的:调用数据库、调用其他API。Java 21将虚拟线程作为正式功能推出,允许你用熟悉的阻塞式代码编写同时能服务大量并发请求。知道什么时候应该以及不应该使用它是面试中的一个明显加分项。

import java.time.Duration;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class PriceAggregator {

    public static void main(String[] args) throws Exception {
        List<String> suppliers = List.of("SupplierA", "SupplierB", "SupplierC");
        long start = System.currentTimeMillis();

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<String>> futures = suppliers.stream()
                .map(s -> executor.submit(() -> fetchPrice(s)))
                .toList();
            for (Future<String> f : futures) {
                System.out.println(f.get());
            }
        }
        System.out.println("总耗时:" + (System.currentTimeMillis() - start) + " ms");
    }

    static String fetchPrice(String supplier) throws InterruptedException {
        Thread.sleep(Duration.ofSeconds(1)); // 模拟缓慢的API调用
        return supplier + " -> 100000 VND, virtual=" + Thread.currentThread().isVirtual();
    }
}
  • newVirtualThreadPerTaskExecutor()为每个任务创建一个虚拟线程。你不需要调整池的大小。
  • try-with-resources块确保executor等待所有任务完成后再关闭。
  • 三个耗时一秒的调用并行运行,所以总时间约为一秒:
SupplierA -> 100000 VND, virtual=true
SupplierB -> 100000 VND, virtual=true
SupplierC -> 100000 VND, virtual=true
总耗时:1015 ms

在Spring Boot 3.2及更高版本,用一行配置启用虚拟线程进行请求处理:

spring.threads.virtual.enabled=true

注意: 虚拟线程不会使CPU密集型代码更快。对围绕长I/O操作的synchronized块要小心,它们可能会将虚拟线程固定在carrier线程上。实际影响取决于JDK版本。另外,同时运行更多任务可能会耗尽数据库连接池。你仍然需要在后端层限制资源。

5. 可观测性:了解你的系统在做什么

当你负责运维时,“为什么慢?“这个问题需要用数据回答。Metrics、logs和traces是三个支柱。在Spring生态中,Micrometer和Actuator是自然的起点。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

添加一个自定义的业务metric:

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final Counter placedOrders;

    public OrderService(MeterRegistry registry) {
        this.placedOrders = Counter.builder("orders.placed")
            .description("成功下单数")
            .tag("channel", "web")
            .register(registry);
    }

    public void placeOrder(String orderId) {
        // ... 保存订单,调用 PaymentService ...
        placedOrders.increment();
    }
}
management.endpoints.web.exposure.include=health,metrics
  • MeterRegistry由Spring Boot自动配置并注入到bean中。
  • Tag channel允许你在查看仪表板时按销售渠道分离指标。
  • 配置行只暴露两个端点health和metrics,不暴露所有端点。

经过几个订单后,调用GET /actuator/metrics/orders.placed:

{
  "name": "orders.placed",
  "description": "成功下单数",
  "measurements": [ { "statistic": "COUNT", "value": 3.0 } ],
  "availableTags": [ { "tag": "channel", "values": [ "web" ] } ]
}

注意: 不要使用数量大且无限制的值(如orderId或customerId)作为标签。每个标签值都会创建一个新的时间序列,所以指标系统会很快膨胀。也不要把整个actuator端点暴露到互联网。

6. 使用Testcontainers用真实基础设施进行测试

当AI能快速生成代码时,测试是证明代码正确的东西。在真实数据库上运行集成测试,而不是不同方言的内存数据库,有助于捕获mock不能捕获的错误。

以下依赖由Spring Boot管理版本:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
</dependency>

最小的Entity和repository:

@Entity
public class Customer {
    @Id @GeneratedValue
    private Long id;
    private String email;

    protected Customer() {}
    public Customer(String email) { this.email = email; }
    public Long getId() { return id; }
    public String getEmail() { return email; }
}

public interface CustomerRepository extends JpaRepository<Customer, Long> {
    Optional<Customer> findByEmail(String email);
}

集成测试:

@SpringBootTest
@Testcontainers
class CustomerRepositoryIT {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @Autowired
    CustomerRepository repository;

    @Test
    void givenSavedCustomer_whenFindByEmail_thenReturnCustomer() {
        repository.save(new Customer("lan@example.com"));

        assertThat(repository.findByEmail("lan@example.com"))
            .isPresent()
            .get()
            .extracting(Customer::getEmail)
            .isEqualTo("lan@example.com");
    }
}
  • @Testcontainers和@Container在测试运行前启动Docker中的真实PostgreSQL。
  • @ServiceConnection(从Spring Boot 3.1开始提供)自动将容器的URL、用户名和密码注入到DataSource中。你不需要@DynamicPropertySource。
  • Query findByEmail由Spring Data生成,在生产环境使用的完全相同的数据库类型上运行。

注意: Testcontainers需要在开发机器和CI上有Docker。测试也比单元测试慢。将大部分逻辑保留在纯单元测试中,仅对repository、messaging和migration这样的集成层使用Testcontainers。

7. 容器打包和运维

理解应用如何打包和运行是使用Kubernetes或云平台的最少技能。一个整洁的多阶段Dockerfile表明你关心镜像大小和安全性:

FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw -q package -DskipTests

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
USER 1000
ENTRYPOINT ["java", "-jar", "app.jar"]
  • build阶段有JDK和Maven wrapper用于编译。
  • 最终阶段只包含JRE和jar文件,所以镜像更小,攻击面更小。
  • USER 1000帮助容器不以root身份运行,这是一个非常常见的错误。

你可以结合第5章的/actuator/health端点作为Kubernetes liveness/readiness probe。

注意: 如果团队已经使用Buildpacks(例如通过./mvnw spring-boot:build-image),你不需要自己写Dockerfile。重要的是理解镜像包含什么、以哪个用户运行,以及内存限制如何影响JVM。

8. 什么时候不应该跟风

投资技能也需要保持理智:

  • 不要忽视基础知识。 数据结构、SQL、HTTP、事务、错误处理仍然是每次面试都会问的东西。AI和框架变化比这些知识快得多。
  • 不要学得太宽。 选择与你当前工作相关的一两项技能,应用到真实项目中,然后再扩展。一个添加metrics和集成测试的PR比十个未使用的证书更有价值。
  • 不要仅为了在简历中出现而引入技术。 一个小系统、少量用户不需要Kubernetes、微服务或LLM。知道什么时候说“不需要”并给出原因也是一项受高度评价的技能。
  • 不要完全交给代码生成工具。 你仍然负责。阅读、理解并为所有生成的代码编写测试。

9. 结论

IT市场奖励那些掌握功能完整生命周期的开发者:设计、编码、测试、打包和运维。本文介绍了五个值得投资的技能组:

  • 在接口后面集成LLM,具有验证和回退功能。
  • 对I/O密集型工作负载使用虚拟线程。
  • 使用Micrometer和Actuator添加可观测性。
  • 使用Testcontainers在真实基础设施上编写集成测试。
  • 安全地打包容器。

每项技能都有其局限性,了解这些局限性和知道如何使用它们一样重要。本周就选择其中一项,立即应用到你正在处理的服务中。这是将趋势转化为真实经验的最快方式。

careertech-trendsjavaspring-bootai