前言
最近在学习Spring Boot和微服务架构,从传统的SSH框架转向更现代的开发方式。整理一下这段时间的开发经验,主要是Spring Boot的一些实践和微服务的踩坑记录。
Spring Boot简介
Spring Boot是Spring团队推出的快速开发框架,目前用的是1.5.8版本。相比传统的Spring项目,配置简化了很多,基本上零配置就能跑起来。
核心特性
- 自动配置 - 根据classpath自动配置Bean
- 起步依赖 - 简化Maven依赖管理
- 内嵌服务器 - 不需要外部Tomcat
- 生产就绪 - 内置监控和健康检查
快速开始
创建项目
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
45
46
47
48
49
50
51
52
53
54
55
56
| <!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<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>user-service</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
|
启动类
1
2
3
4
5
6
7
8
9
10
11
12
| // Application.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
|
配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/userdb?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
database-platform: org.hibernate.dialect.MySQL5Dialect
logging:
level:
com.example: DEBUG
|
RESTful API开发
实体类
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
45
46
47
48
49
50
51
52
| // User.java
package com.example.entity;
import javax.persistence.*;
import java.util.Date;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created_at")
private Date createdAt;
// 构造函数
public User() {}
public User(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
this.createdAt = new Date();
}
// getter和setter方法
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Date getCreatedAt() { return createdAt; }
public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
}
|
Repository层
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
| // UserRepository.java
package com.example.repository;
import com.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 根据用户名查找
Optional<User> findByUsername(String username);
// 根据邮箱查找
Optional<User> findByEmail(String email);
// 自定义查询
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword% OR u.email LIKE %:keyword%")
List<User> searchUsers(@Param("keyword") String keyword);
// 统计用户数量
@Query("SELECT COUNT(u) FROM User u")
Long countUsers();
}
|
Service层
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
| // UserService.java
package com.example.service;
import com.example.entity.User;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 创建用户
*/
public User createUser(User user) {
// 检查用户名是否已存在
if (userRepository.findByUsername(user.getUsername()).isPresent()) {
throw new RuntimeException("用户名已存在");
}
// 检查邮箱是否已存在
if (userRepository.findByEmail(user.getEmail()).isPresent()) {
throw new RuntimeException("邮箱已存在");
}
return userRepository.save(user);
}
/**
* 根据ID查找用户
*/
@Transactional(readOnly = true)
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
/**
* 根据用户名查找用户
*/
@Transactional(readOnly = true)
public Optional<User> findByUsername(String username) {
return userRepository.findByUsername(username);
}
/**
* 获取所有用户
*/
@Transactional(readOnly = true)
public List<User> findAllUsers() {
return userRepository.findAll();
}
/**
* 搜索用户
*/
@Transactional(readOnly = true)
public List<User> searchUsers(String keyword) {
return userRepository.searchUsers(keyword);
}
/**
* 更新用户
*/
public User updateUser(Long id, User userDetails) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("用户不存在"));
user.setUsername(userDetails.getUsername());
user.setEmail(userDetails.getEmail());
return userRepository.save(user);
}
/**
* 删除用户
*/
public void deleteUser(Long id) {
if (!userRepository.existsById(id)) {
throw new RuntimeException("用户不存在");
}
userRepository.deleteById(id);
}
}
|
Controller层
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
| // UserController.java
package com.example.controller;
import com.example.entity.User;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
/**
* 创建用户
*/
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
try {
User createdUser = userService.createUser(user);
return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
} catch (RuntimeException e) {
return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
}
}
/**
* 获取所有用户
*/
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.findAllUsers();
return new ResponseEntity<>(users, HttpStatus.OK);
}
/**
* 根据ID获取用户
*/
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
Optional<User> user = userService.findById(id);
if (user.isPresent()) {
return new ResponseEntity<>(user.get(), HttpStatus.OK);
} else {
return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
}
}
/**
* 搜索用户
*/
@GetMapping("/search")
public ResponseEntity<List<User>> searchUsers(@RequestParam String keyword) {
List<User> users = userService.searchUsers(keyword);
return new ResponseEntity<>(users, HttpStatus.OK);
}
/**
* 更新用户
*/
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User userDetails) {
try {
User updatedUser = userService.updateUser(id, userDetails);
return new ResponseEntity<>(updatedUser, HttpStatus.OK);
} catch (RuntimeException e) {
return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
}
}
/**
* 删除用户
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
try {
userService.deleteUser(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} catch (RuntimeException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
}
|
微服务架构实践
服务注册与发现 - Eureka
1
2
3
4
5
| <!-- 注册中心依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
|
1
2
3
4
5
6
7
8
| // EurekaServerApplication.java
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
| # eureka-server配置
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
|
服务提供者配置
1
2
3
4
5
| <!-- 服务提供者依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
|
1
2
3
4
5
6
7
8
| // 启动类添加注解
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
| # 服务提供者配置
spring:
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
|
服务调用 - Feign
1
2
3
4
5
| <!-- Feign依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| // UserServiceClient.java
@FeignClient(name = "user-service")
public interface UserServiceClient {
@GetMapping("/api/users/{id}")
User getUserById(@PathVariable("id") Long id);
@PostMapping("/api/users")
User createUser(@RequestBody User user);
@GetMapping("/api/users")
List<User> getAllUsers();
}
|
1
2
3
4
5
6
7
8
9
| // 启动类启用Feign
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
|
配置中心 - Spring Cloud Config
1
2
3
4
5
| <!-- Config Server依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
|
1
2
3
4
5
6
7
8
| // ConfigServerApplication.java
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
| # config-server配置
server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://github.com/your-repo/config-repo
search-paths: config
|
监控和健康检查
Actuator监控
1
2
3
4
5
| <!-- Actuator依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
|
1
2
3
4
5
6
7
8
9
10
11
| # 监控配置
management:
security:
enabled: false
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
|
自定义健康检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // CustomHealthIndicator.java
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
// 检查数据库连接
if (isDatabaseHealthy()) {
return Health.up()
.withDetail("database", "可用")
.withDetail("diskSpace", "充足")
.build();
} else {
return Health.down()
.withDetail("database", "不可用")
.build();
}
}
private boolean isDatabaseHealthy() {
// 实际的数据库健康检查逻辑
return true;
}
}
|
异常处理
全局异常处理
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
45
46
47
| // GlobalExceptionHandler.java
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException e) {
ErrorResponse error = new ErrorResponse(
"RUNTIME_ERROR",
e.getMessage(),
System.currentTimeMillis()
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException e) {
ErrorResponse error = new ErrorResponse(
"INVALID_ARGUMENT",
e.getMessage(),
System.currentTimeMillis()
);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
}
// ErrorResponse.java
public class ErrorResponse {
private String code;
private String message;
private Long timestamp;
public ErrorResponse(String code, String message, Long timestamp) {
this.code = code;
this.message = message;
this.timestamp = timestamp;
}
// getter和setter方法
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public Long getTimestamp() { return timestamp; }
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
}
|
日志配置
Logback配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| <!-- logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.example" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
|
测试
单元测试
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
| // UserServiceTest.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@MockBean
private UserRepository userRepository;
@Test
public void testCreateUser() {
// 准备测试数据
User user = new User("testuser", "password", "test@example.com");
// Mock repository行为
when(userRepository.findByUsername("testuser")).thenReturn(Optional.empty());
when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.empty());
when(userRepository.save(any(User.class))).thenReturn(user);
// 执行测试
User createdUser = userService.createUser(user);
// 验证结果
assertNotNull(createdUser);
assertEquals("testuser", createdUser.getUsername());
assertEquals("test@example.com", createdUser.getEmail());
}
@Test(expected = RuntimeException.class)
public void testCreateUserWithDuplicateUsername() {
User existingUser = new User("testuser", "password", "existing@example.com");
User newUser = new User("testuser", "newpassword", "new@example.com");
when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(existingUser));
userService.createUser(newUser);
}
}
|
集成测试
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
| // UserControllerIntegrationTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class UserControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@Before
public void setUp() {
userRepository.deleteAll();
}
@Test
public void testCreateAndGetUser() {
// 创建用户
User user = new User("testuser", "password", "test@example.com");
ResponseEntity<User> createResponse = restTemplate.postForEntity("/api/users", user, User.class);
assertEquals(HttpStatus.CREATED, createResponse.getStatusCode());
assertNotNull(createResponse.getBody());
Long userId = createResponse.getBody().getId();
// 获取用户
ResponseEntity<User> getResponse = restTemplate.getForEntity("/api/users/" + userId, User.class);
assertEquals(HttpStatus.OK, getResponse.getStatusCode());
assertEquals("testuser", getResponse.getBody().getUsername());
}
}
|
部署和运维
Docker化
1
2
3
4
5
6
7
8
9
10
| # Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY target/user-service-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"]
|
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
| # docker-compose.yml
version: '3'
services:
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: userdb
ports:
- "3306:3306"
eureka-server:
build: ./eureka-server
ports:
- "8761:8761"
user-service:
build: ./user-service
ports:
- "8080:8080"
depends_on:
- mysql
- eureka-server
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/userdb?useSSL=false
EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE: http://eureka-server:8761/eureka/
|
生产环境配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| # application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-mysql:3306/userdb?useSSL=true
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
logging:
level:
root: WARN
com.example: INFO
file: /var/log/user-service.log
management:
security:
enabled: true
|
常见问题
循环依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 避免循环依赖的方法
@Service
public class UserService {
@Lazy
@Autowired
private OrderService orderService;
// 或者使用@PostConstruct
private OrderService orderService;
@PostConstruct
public void init() {
this.orderService = ApplicationContextUtils.getBean(OrderService.class);
}
}
|
事务失效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 事务失效的常见原因和解决方法
@Service
public class UserService {
// 错误:同类内部调用,事务不生效
public void methodA() {
this.methodB(); // 事务不生效
}
@Transactional
public void methodB() {
// 数据库操作
}
// 正确:通过代理调用
@Autowired
private UserService self;
public void methodA() {
self.methodB(); // 事务生效
}
}
|
配置优先级
1
2
3
4
5
6
7
8
9
10
11
12
| 配置优先级(从高到低):
1. 命令行参数
2. SPRING_APPLICATION_JSON
3. ServletConfig初始化参数
4. ServletContext初始化参数
5. JNDI属性
6. Java系统属性
7. 操作系统环境变量
8. application-{profile}.properties
9. application.properties
10. @PropertySource注解
11. 默认属性
|
总结
Spring Boot确实简化了很多开发工作,特别是配置方面。微服务架构虽然复杂一些,但是对于大型项目来说,好处还是很明显的。
几个要点:
- 合理使用自动配置,但要理解背后的原理
- 微服务拆分要合理,不要为了微服务而微服务
- 监控和日志很重要,生产环境必须要有
- 测试要跟上,单元测试和集成测试都不能少
- Docker化部署能简化运维工作
目前Spring Boot 1.5.x版本已经比较稳定,Spring Cloud也在快速发展。后面准备研究一下Spring Boot 2.0的新特性,听说性能提升不少。
参考资料