目录

Spring Boot 实践推荐

Spring Boot 是最流行的用于开发微服务的 Java 框架。在使用过程中有一些实践总结。

自定义BOM来维护第三方依赖

Spring Boot项目本身使用和集成了大量的开源项目,它帮助我们维护了这些第三方依赖。但是也有一部分在实际项目使用中并没有包括进来,这就需要我们在项目中自己维护版本。

参考 Spring Boot 项目 中 spring-boot-dependencies 的 pom 维护一个自己的 <packaging>pom</packaging> 三方依赖项目。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.ynthm.boot</groupId>
      <artifactId>third-dependencies</artifactId>
      <version>1.0.0.RELEASE</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<modelVersion>4.0.0</modelVersion>
<groupId>com.ynthm.boot</groupId>
<artifactId>third-dependencies</artifactId>
<version>1.0.0.RELEASE</version>
<packaging>pom</packaging>
<name>third-dependencies</name>

<properties>
  <guava.version>31.1-jre</guava.version>
  <maven-jar-plugin.version>3.2.0</maven-jar-plugin.version>
  <maven-javadoc-plugin.version>3.2.0</maven-javadoc-plugin.version>
  <maven-source-plugin.version>3.2.1</maven-source-plugin.version>  
</properties>
<dependencyManagement>
  <dependencies>
      <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>${guava.version}</version>
    </dependency>
  </dependencies>
</dependencyManagement>
  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-jar-plugin</artifactId>
          <version>${maven-jar-plugin.version}</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-javadoc-plugin</artifactId>
          <version>${maven-javadoc-plugin.version}</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-source-plugin</artifactId>
          <version>${maven-source-plugin.version}</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

使用自动配置

Spring Boot 的一个主要特性是使用自动配置。使用它的最简单方法是依赖Spring Boot Starters。

1
2
3
4
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

通过使用以下注解属性,可以从自动配置中排除某些配置类,必要时才应该这样做。

1
@EnableAutoConfigurationexclude = {ClassNotToAutoconfigure.class}

Spring Initializr 初始化项目

Spring Initializr 提供了一个超级简单的方法来创建一个新的Spring Boot项目,并根据你的需要来加载可能使用到的依赖。

IDEA 新建项目的时候集成了一个 Spring Initializr。

创建自己的自动配置 starter 项目

如果你在一个严重依赖Spring Boot的公司或团队中工作,并且有共同的问题需要解决,那么你可以创建自己的自动配置。

这项任务涉及较多工作,因此你需要考虑何时获益是值得投入的。与多个略有不同的定制配置相比,维护单个自动配置更容易。

如果将这个提供Spring Boot配置以开源库的形式发布出去,那么将极大地简化数千个用户的配置工作。

官方 spring-boot-starter-data-redis spring-boot-starter 在前面 非官方mybatis-spring-boot-starter 在后面。

正确设计代码目录结构

尽管允许你有很大的自由,但是有一些基本规则值得遵守来设计你的源代码结构。

首先你要依据 maven 结构组织项目。 避免使用默认包。确保所有内容(包括你的入口点)都位于一个名称很好的包中,这样就可以避免与装配和组件扫描相关的意外情况; 将Application.java(应用的入口类)保留在顶级源代码目录中。 将控制器和服务放在以功能为导向的模块中,但这是可选的。

保持@Controller的简洁和专注

  • 控制器应该是无状态的!默认情况下,控制器是单例,并且任何状态都可能导致大量问题;
  • 控制器不应该执行业务逻辑,而是依赖委托;
  • 控制器应该处理应用程序的HTTP层,这不应该传递给服务;
  • 控制器应该围绕用例/业务能力来设计。

要深入这个内容,需要进一步地了解设计REST API的最佳实践。

围绕业务功能构建 @Service

Service是Spring Boot的另一个核心概念。我发现最好围绕业务功能/领域/用例(无论你怎么称呼都行)来构建服务。

在应用中设计名称类似AccountService, UserService, PaymentService这样的服务,比起像DatabaseService、ValidationService、CalculationService这样的会更合适一些。

你可以决定使用Controler和Service之间的一对一映射,那将是理想的情况。但这并不意味着,Service之间不能互相调用!

使数据库独立于核心业务逻辑之外

你的数据库是一个“细节”,这意味着不将你的应用程序与特定数据库耦合。过去很少有人会切换数据库,我注意到,使用Spring Boot和现代微服务开发会让事情变得更快。

保持业务逻辑不受Spring Boot代码的影响

推荐使用构造函数注入

保持业务逻辑免受Spring Boot代码侵入的一种方法是使用构造函数注入。不仅是因为@Autowired注解在构造函数上是可选的,而且还可以在没有Spring的情况下轻松实例化bean。

熟悉并发模型

在Spring Boot中,Controller和Service是默认是单例。如果你不小心,这会引入可能的并发问题。你通常也在处理有限的线程池。请熟悉这些概念。

在考虑 Spring Boot 应用程序中的并发性时,值得考虑的关键领域是:

  • 最大线程数—— 这是为处理对应用程序的请求而分配的最大线程数
  • 共享外部资源 ——调用外部共享资源,如数据库和其他 REST 端点可能需要大量时间。
  • 异步方法调用 ——这些方法调用在等待响应时将线程释放回线程池
  • 共享内部资源 ——调我们通常无法控制外部资源的事情,但我们完全控制了系统的内部资源。
# 最大工作线程数,默认200。
server.tomcat.max-threads=200

# 最大连接数默认是10000
server.tomcat.max-connections=10000

# 等待队列长度,默认100。
server.tomcat.accept-count=100

# 最小工作空闲线程数,默认10。
server.tomcat.min-spare-threads=100

Tomcat有两种处理连接的模式

  • 一种是BIO,一个线程只处理一个Socket连接;缺省是200

  • 另一种就是NIO,一个线程处理多个Socket连接。 默认是10000

  • maxThreads是指Tomcat线程池最多能起的线程数

  • maxConnections则是Tomcat一瞬间最多能够处理的并发连接数。并发量指的是连接数

  • 多开线程的代价就是增加上下文切换的时间,浪费CPU时间。另外还有就是线程数增多,每个线程分配到的时间片就变少。多开线程并不等于提高处理效率。

  • 增加最大连接数,支持的并发量确实可以上去。但是在没有改变硬件条件的情况下,这种并发量的提升必定以牺牲响应时间为代价。

  • 当连接数达到最大值maxConnections后,系统会继续接收连接,进行排队,但不会超过acceptCount的值。

  • 当队列(acceptCount)已满时,任何的连接请求都将被拒绝。acceptCount的默认值为100。

  • 线程数的经验值为:1核2G内存,线程数经验 值 200;4 核 8G 内 存 , 线 程 数 经 验 值800。

  • 等待队列长度:队列做缓冲池用,但也不能无限长,消耗内存,出入队列也耗CPU。

@EnableAsync 注释下的 Application 类上的@SpringBootApplication 注释开始。启用后,您可以@Async 在返回的服务中使用注释 CompletableFuture<>,这些@Async 方法将在后台线程池中运行。

Spring 服务和控制器默认是单例的。共享状态的其他潜在来源是缓存和自定义的、服务器范围的组件(通常是监控、安全等)。

加强配置管理的外部化

  • 使用配置服务器,例如Spring Cloud Config;
  • 将所有配置存储在环境变量中(可以基于git仓库进行配置)。

这些选项中的任何一个(第二个选项多一些)都要求你在DevOps更少工作量,但这在微服务领域是很常见的。

提供全局异常处理

Spring Boot 提供了两种主要方法:

  • @RestControllerAdvice + @ExceptionHandler
  • 自定义 HandlerExceptionResolver 全局异常处理策略;
  • @Controller 的方法添加 @ExceptionHandler 注解,这在某些特定场景下使用可能会很有用。
  • ResponseStatusException Spring Boot 5
  • Handle the Access Denied in Spring Security
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
    try {
        Foo resourceById = RestPreconditions.checkFound(service.findOne(id));

        eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
        return resourceById;
     }
    catch (MyResourceNotFoundException exc) {
         throw new ResponseStatusException(
           HttpStatus.NOT_FOUND, "Foo Not Found", exc);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle
      (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) 
      throws IOException, ServletException {
        response.sendRedirect("/my-error-page");
    }
}

使用日志框架

1
Logger logger = LoggerFactory.getLogger(MyClass.class);

测试你的代码

这不是Spring Boot特有的,但它需要提醒——测试你的代码!如果你没有编写测试,那么你将从一开始就编写遗留代码。

如果有其他人使用你的代码库,那边改变任何东西将会变得危险。当你有多个服务相互依赖时,这甚至可能更具风险。

由于存在Spring Boot最佳实践,因此你应该考虑将Spring Cloud Contract用于你的消费者驱动契约,它将使你与其他服务的集成更容易使用。

使用测试切片让测试更容易,并且更专注

测试切片是关于分割 ApplicationContext 为您的测试创建的。通常,如果您想使用 测试控制器 MockMvc,您肯定不想打扰数据层。相反,您可能想要模拟您的控制器使用的服务并验证所有与 Web 相关的交互是否按预期工作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@RunWith(SpringRunner.class)
@WebMvcTest(UserVehicleController.class)
public class UserVehicleControllerTests {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private UserVehicleService userVehicleService;

    @Test
    public void testExample() throws Exception {
        given(this.userVehicleService.getVehicleDetails("sboot"))
                .willReturn(new VehicleDetails("Honda", "Civic"));
        this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
                .andExpect(status().isOk()).andExpect(content().string("Honda Civic"));
    }
}

@WebMvcTest 是 Spring Boot 1.4 中的 web 测试切片。当它存在时,您指示 Spring Boot 需要一个 Web 环境,并且只应实例化指定的控制器。因为它知道测试的性质,它可以为您做出额外的明智决定(例如,自动配置MockMvc以便剩下的就是注入它)。此外,您的控制器具有依赖关系,UserVehicleService因此启动上下文会导致失败,因为ApplicationContext不知道它(请记住,只有 Web 基础架构UserVehicleController是已知的)。@MockBean这里用来注册一个UserVehicleServicemock,以便可以透明的注入到控制器中。

你也可以创建自己的切片