Spring开发最佳实践与Kotlin应用
1. 依赖注入最佳实践
Bean有两种依赖类型:
-
强制依赖
:是Bean必需的依赖,如果依赖不可用,上下文加载会失败。
-
可选依赖
:是可选的依赖,即使不可用,上下文也能正常加载。
建议使用构造函数注入来处理强制依赖,而不是setter注入。这样可以确保在缺少强制依赖时,上下文无法加载。示例代码如下:
public class SomeClass {
private MandatoryDependency mandatoryDependency;
private OptionalDependency optionalDependency;
public SomeClass(MandatoryDependency mandatoryDependency) {
this.mandatoryDependency = mandatoryDependency;
}
public void setOptionalDependency(OptionalDependency optionalDependency) {
this.optionalDependency = optionalDependency;
}
//All other logic
}
Spring团队通常提倡构造函数注入,因为它能将应用组件实现为不可变对象,并确保所需依赖不为空。此外,构造函数注入的组件始终以完全初始化的状态返回给客户端代码。不过,大量的构造函数参数是一个不好的代码信号,意味着该类可能承担了过多的职责,应该进行重构以更好地实现关注点分离。Setter注入主要用于可以在类中分配合理默认值的可选依赖。否则,在代码使用依赖的地方必须进行非空检查。Setter注入的一个好处是,setter方法使该类的对象便于后续重新配置或重新注入。通过JMX MBeans进行管理就是Setter注入的一个有说服力的用例。
2. 管理Spring项目的依赖版本
-
使用Spring Boot
:最简单的管理依赖版本的方法是使用
spring-boot-starter-parent作为父POM。示例如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring-boot.version}</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
spring-boot-starter-parent
管理着200多个依赖的版本。在Spring Boot发布之前,会确保这些依赖的所有版本能够很好地协同工作。部分管理的依赖版本如下表所示:
| 依赖 | 版本 |
| ---- | ---- |
| activemq | 5.14.3 |
| ehcache | 2.10.3 |
| elasticsearch | 2.4.4 |
| h2 | 1.4.193 |
| jackson | 2.8.7 |
| jersey | 2.25.1 |
| junit | 4.12 |
| mockito | 1.10.19 |
| mongodb | 3.4.2 |
| mysql | 5.1.41 |
| reactor | 2.0.8.RELEASE |
| reactor-spring | 2.0.7.RELEASE |
| selenium | 2.53.1 |
| spring | 4.3.7.RELEASE |
| spring-amqp | 1.7.1.RELEASE |
| spring-cloud-connectors | 1.2.3.RELEASE |
| spring-batch | 3.0.7.RELEASE |
| spring-hateoas | 0.23.0.RELEASE |
| spring-kafka | 1.1.3.RELEASE |
| spring-restdocs | 1.1.2.RELEASE |
| spring-security | 4.2.2.RELEASE |
| thymeleaf | 2.1.5.RELEASE |
建议不要在项目POM文件中覆盖这些管理的依赖版本。这样可以确保在升级Spring Boot版本时,能获得所有依赖的最新版本升级。
-
使用自定义企业POM
:如果必须使用自定义企业POM作为父POM,可以使用以下配置来管理依赖版本:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 不使用Spring Boot :可以使用Spring BOM来管理所有基本的Spring依赖:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>${org.springframework-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3. 单元测试
单元测试的基本目的是发现缺陷,但不同层的单元测试编写方法不同。
-
业务层
:编写业务层测试时,建议避免在单元测试中使用Spring Framework,这样可以确保测试独立于框架,并且运行速度更快。示例代码如下:
@RunWith(MockitoJUnitRunner.class)
public class BusinessServiceMockitoTest {
private static final User DUMMY_USER = new User("dummy");
@Mock
private DataService dataService;
@InjectMocks
private BusinessService service = new BusinessServiceImpl();
@Test
public void testCalculateSum() {
BDDMockito.given(dataService.retrieveData(Matchers.any(User.class)))
.willReturn(Arrays.asList(new Data(10), new Data(15), new Data(25)));
long sum = service.calculateSum(DUMMY_USER);
assertEquals(10 + 15 + 25, sum);
}
}
Spring Framework用于在运行的应用程序中连接依赖。但在单元测试中,结合使用
@InjectMocks
和
@Mock
Mockito注解是最佳选择。
-
Web层
:Web层的单元测试涉及测试控制器(REST和其他类型)。建议如下:
- 对于基于Spring MVC构建的Web层,使用Mock MVC。
- 对于使用Jersey和JAX - RS构建的REST服务,Jersey Test Framework是一个不错的选择。
以下是设置Mock MVC框架的示例:
@RunWith(SpringRunner.class)
@WebMvcTest(TodoController.class)
public class TodoControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private TodoService service;
//Tests
}
使用
@WebMvcTest
可以自动注入
MockMvc
并执行Web请求。
@WebMvcTest
的一个重要特性是它只实例化控制器组件,其他Spring组件需要进行模拟并使用
@MockBean
进行自动注入。
-
数据层
:Spring Boot为数据层单元测试提供了简单的注解
@DataJpaTest
。示例如下:
@DataJpaTest
@RunWith(SpringRunner.class)
public class UserRepositoryTest {
@Autowired
UserRepository userRepository;
@Autowired
TestEntityManager entityManager;
//Test Methods
}
@DataJpaTest
还可以注入
TestEntityManager
bean,它是专门为测试设计的标准JPA
entityManager
的替代方案。如果想在
@DataJpaTest
之外使用
TestEntityManager
,可以使用
@AutoConfigureTestEntityManager
注解。数据JPA测试默认在嵌入式数据库上运行,这确保了测试可以多次运行而不影响数据库。
4. 其他单元测试最佳实践
- 单元测试应该具有可读性,其他开发人员应该能够在不到15秒的时间内理解测试。目标是使测试能够作为代码的文档。
- 单元测试应该仅在生产代码存在缺陷时失败。如果单元测试使用外部数据,当外部数据发生变化时,测试可能会失败,随着时间的推移,开发人员会对单元测试失去信心。
- 单元测试应该运行快速。缓慢的测试很少被运行,会失去单元测试的所有好处。
- 单元测试应该作为持续集成的一部分运行。一旦在版本控制中进行了提交,构建(包括单元测试)就应该运行,并在失败时通知开发人员。
5. 集成测试
单元测试用于测试特定的层,而集成测试用于测试多层代码。为了使测试具有可重复性,建议在集成测试中使用嵌入式数据库而不是真实数据库。建议为集成测试创建一个使用嵌入式数据库的单独配置文件,这样每个开发人员都可以有自己的数据库来运行测试。
以下是相关配置文件示例:
-
application.properties
:
app.profiles.active: production
-
application-production.properties:
app.jpa.database: MYSQL
app.datasource.url: <<VALUE>>
app.datasource.username: <<VALUE>>
app.datasource.password: <<VALUE>>
-
application-integration-test.properties:
app.jpa.database: H2
app.datasource.url=jdbc:h2:mem:mydb
app.datasource.username=sa
app.datasource.pool-size=30
需要在测试范围中包含H2驱动依赖,示例如下:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
以下是一个使用
@ActiveProfiles("integration-test")
的集成测试示例:
@ActiveProfiles("integration-test")
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TodoControllerIT {
@LocalServerPort
private int port;
private TestRestTemplate template = new TestRestTemplate();
//Tests
}
集成测试对于持续交付可用的软件至关重要。Spring Boot提供的功能使实现集成测试变得容易。
6. Spring Session
管理会话状态是分布式和扩展Web应用程序的重要挑战之一。HTTP是无状态协议,用户与Web应用程序交互的状态通常在
HttpSession
中管理。重要的是在会话中存储尽可能少的数据,专注于识别和删除会话中不需要的数据。
考虑一个具有三个实例的分布式应用程序,每个实例都有自己的本地会话副本。如果用户当前由应用实例1提供服务,而实例1出现故障,负载均衡器将用户发送到应用实例2,实例2不知道实例1中可用的会话状态,用户必须重新登录并重新开始,这不是一个好的用户体验。
Spring Session提供了将会话存储外部化的功能。它不是使用本地
HttpSession
,而是提供了将会话状态存储到不同数据存储的替代方案。Spring Session还提供了清晰的关注点分离,无论使用哪种会话数据存储,应用程序代码都保持不变。可以通过配置在不同的会话数据存储之间进行切换。
以下是连接Spring Session使用Redis会话存储的示例:
-
添加Spring Session的依赖
:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<type>pom</type>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
-
配置过滤器以用Spring Session替换
HttpSession:
@EnableRedisHttpSession
public class ApplicationConfiguration {
@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory();
}
}
-
通过扩展
AbstractHttpSessionApplicationInitializer为Tomcat启用过滤 :
public class Initializer extends AbstractHttpSessionApplicationInitializer {
public Initializer() {
super(ApplicationConfiguration.class);
}
}
这样配置后,应用程序代码与
HttpSession
交互的部分无需更改,仍然可以使用
HttpSession
接口,但在后台,Spring Session会确保会话数据存储到外部数据存储(在这个例子中是Redis):
req.getSession().setAttribute(name, value);
Spring Session提供了简单的选项来连接外部会话存储,将会话备份到外部会话存储可以确保即使应用程序的某个实例出现故障,用户也可以继续使用。
7. 缓存
缓存对于构建高性能应用程序至关重要。不希望每次都访问外部服务或数据库,可以缓存不经常更改的数据。Spring提供了透明的机制来连接和使用缓存。启用应用程序缓存的步骤如下:
-
添加Spring Boot Starter Cache依赖
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
- 添加缓存注解 :
@Component
public class ExampleRepository implements Repository {
@Override
@Cacheable("something-cache-key")
public Something getSomething(String id) {
//Other code
}
}
支持的一些注解如下:
| 注解 | 说明 |
| ---- | ---- |
|
@Cacheable
| 用于缓存方法调用的结果。默认实现根据传递给方法的参数构造键。如果在缓存中找到值,则不会调用该方法。 |
|
@CachePut
| 与
@Cacheable
类似,但不同的是,该方法总是被调用,并且结果会被放入缓存中。 |
|
@CacheEvict
| 触发从缓存中移除特定元素。通常在元素被删除或更新时执行。 |
其他需要注意的事项:
- 默认使用的缓存是
ConcurrentHashMap
。
- Spring缓存抽象符合JSR - 107标准。
- 其他可以自动配置的缓存包括EhCache、Redis和Hazelcast。
8. 日志
Spring和Spring Boot依赖于Commons Logging API,不依赖于其他日志框架。Spring Boot提供了启动器来简化特定日志框架的配置。
-
Logback
:使用
spring-boot-starter-logging
启动器即可使用Logback框架。该依赖是大多数启动器(包括
spring-boot-starter-web
)中包含的默认日志依赖。示例如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
spring-boot-starter-logging
包含的Logback及相关依赖如下:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
</dependency>
-
Log4j2
:要使用Log4j2,需要使用
spring-boot-starter-log4j2启动器。使用spring-boot-starter-web等启动器时,需要确保排除spring-boot-starter-logging依赖。示例如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
spring-boot-starter-log4j2
启动器使用的依赖如下:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
</dependency>
- 框架独立配置 :无论使用哪种日志框架,Spring Boot都允许在应用程序属性中进行一些基本的配置。示例如下:
logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate=ERROR
logging.file=<<PATH_TO_LOG_FILE>>
在微服务时代,无论使用哪种日志框架,建议将日志记录到控制台(而不是文件),并使用集中式日志存储工具来捕获所有微服务实例的日志。
9. Kotlin简介
Kotlin是一种静态类型的JVM语言,使代码具有表现力、简洁性和可读性。Spring Framework 5.0对Kotlin有很好的支持。Kotlin是一种开源的静态类型语言,可用于构建在JVM、Android和JavaScript平台上运行的应用程序。它由JetBrains根据Apache 2.0许可证开发,源代码可在GitHub(https://github.com/jetbrains/kotlin)上获取。后续将探讨Kotlin的一些重要特性,并学习如何使用Kotlin和Spring Boot创建基本的REST服务。
Spring开发最佳实践与Kotlin应用
10. 使用Kotlin创建Spring Boot项目
在了解了Spring开发的各项最佳实践后,接下来看看如何使用Kotlin创建Spring Boot项目。以下为具体步骤:
-
创建项目骨架 :可以使用Spring Initializr(https://start.spring.io/ )来创建一个新的Spring Boot项目。在该网站上,选择Kotlin作为语言,添加所需的依赖,如Spring Web等,然后生成项目压缩包并解压到本地。
-
配置构建文件 :如果使用Maven,项目的
pom.xml文件会包含Kotlin相关的配置和依赖。以下是一个简单的示例:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>kotlin-spring-boot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<kotlin.version>1.7.20</kotlin.version>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
-
创建主应用类
:在
src/main/kotlin目录下创建主应用类,示例如下:
package com.example.kotlinspringboot
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class KotlinSpringBootApplication
fun main(args: Array<String>) {
runApplication<KotlinSpringBootApplication>(*args)
}
11. 实现简单的Spring Boot REST服务
创建好项目后,我们来实现一个简单的REST服务。
- 定义数据模型 :创建一个简单的数据类来表示资源,例如:
package com.example.kotlinspringboot.model
data class Greeting(val id: Long, val content: String)
- 创建控制器 :创建一个控制器类来处理HTTP请求,示例如下:
package com.example.kotlinspringboot.controller
import com.example.kotlinspringboot.model.Greeting
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.util.concurrent.atomic.AtomicLong
@RestController
class GreetingController {
private val counter = AtomicLong()
@GetMapping("/greeting")
fun greeting(@RequestParam(value = "name", defaultValue = "World") name: String): Greeting {
return Greeting(counter.incrementAndGet(), "Hello, $name!")
}
}
12. Kotlin实现的单元测试
对于使用Kotlin实现的REST服务,同样需要进行单元测试。以下是一个使用MockMvc进行单元测试的示例:
package com.example.kotlinspringboot.controller
import com.example.kotlinspringboot.model.Greeting
import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.Test
import org.mockito.Mockito.`when`
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
@WebMvcTest(GreetingController::class)
class GreetingControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var controller: GreetingController
@Test
fun testGreeting() {
val id = 1L
val content = "Hello, World!"
val greeting = Greeting(id, content)
`when`(controller.greeting("World")).thenReturn(greeting)
mockMvc.perform(get("/greeting").param("name", "World"))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id").value(id))
.andExpect(jsonPath("$.content").value(content))
}
}
13. 总结
本文涵盖了Spring开发的多个最佳实践,包括依赖注入、依赖版本管理、单元测试、集成测试、会话管理、缓存、日志等方面,同时介绍了Kotlin的基本概念以及如何使用Kotlin创建Spring Boot项目和实现REST服务。
以下是Spring开发最佳实践的总结表格:
| 实践领域 | 最佳实践要点 |
| ---- | ---- |
| 依赖注入 | 强制依赖使用构造函数注入,可选依赖使用Setter注入 |
| 依赖版本管理 | Spring Boot使用
spring-boot-starter-parent
,自定义POM使用
spring-boot-dependencies
,非Spring Boot使用Spring BOM |
| 单元测试 | 业务层避免使用Spring Framework,Web层根据情况选择Mock MVC或Jersey Test Framework,数据层使用
@DataJpaTest
|
| 集成测试 | 使用嵌入式数据库,创建单独的配置文件 |
| 会话管理 | 使用Spring Session将会话存储外部化 |
| 缓存 | 添加
spring-boot-starter-cache
依赖并使用缓存注解 |
| 日志 | 选择合适的日志框架,建议记录到控制台并使用集中式存储 |
| Kotlin应用 | 使用Spring Initializr创建项目,使用Kotlin实现REST服务并进行单元测试 |
mermaid格式流程图展示Spring开发项目的整体流程:
graph LR
A[项目初始化] --> B[依赖注入配置]
B --> C[依赖版本管理]
C --> D[单元测试编写]
D --> E[集成测试编写]
E --> F[会话管理配置]
F --> G[缓存配置]
G --> H[日志配置]
H --> I[Kotlin应用开发]
通过遵循这些最佳实践,可以提高Spring项目的开发效率、可维护性和性能,同时利用Kotlin的优势,使代码更加简洁和易读。在实际开发中,应根据项目的具体需求和场景,灵活运用这些技术和方法。
6947

被折叠的 条评论
为什么被折叠?



