第2章 服务注册与发现

2.1 服务注册与发现概述

什么是服务注册与发现

在微服务架构中,服务注册与发现是一个核心概念。随着服务数量的增加和动态扩缩容的需求,手动管理服务实例的地址变得不现实。服务注册与发现机制解决了以下问题:

  1. 服务定位:如何找到可用的服务实例
  2. 负载均衡:如何在多个服务实例间分配请求
  3. 故障检测:如何检测和剔除不健康的服务实例
  4. 动态配置:如何处理服务实例的动态变化

服务注册与发现架构

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   服务提供者A    │    │   服务提供者B    │    │   服务提供者C    │
│   (User Service) │    │ (Order Service) │    │(Product Service)│
└─────────┬───────┘    └─────────┬───────┘    └─────────┬───────┘
          │ 注册                  │ 注册                  │ 注册
          ▼                      ▼                      ▼
┌─────────────────────────────────────────────────────────────────┐
│                    服务注册中心                                    │
│                 (Service Registry)                              │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐ │
│  │ User Service│ │Order Service│ │     Product Service         │ │
│  │ 192.168.1.10│ │ 192.168.1.20│ │ 192.168.1.30,192.168.1.31 │ │
│  │    :8081    │ │    :8082    │ │      :8083,:8084           │ │
│  └─────────────┘ └─────────────┘ └─────────────────────────────┘ │
└─────────────────────┬───────────────────────────────────────────┘
                      │ 发现
                      ▼
            ┌─────────────────┐
            │   服务消费者     │
            │ (API Gateway)   │
            └─────────────────┘

核心组件

  1. 服务注册中心(Service Registry)

    • 存储服务实例的元数据信息
    • 提供服务注册和注销接口
    • 健康检查和故障检测
  2. 服务提供者(Service Provider)

    • 启动时向注册中心注册自己
    • 定期发送心跳保持注册状态
    • 关闭时注销服务
  3. 服务消费者(Service Consumer)

    • 从注册中心获取服务实例列表
    • 根据负载均衡策略选择服务实例
    • 缓存服务实例信息

2.2 Eureka 服务注册中心

Eureka 架构

Eureka是Netflix开源的服务发现组件,采用AP(可用性和分区容错性)设计,适合云环境下的服务发现。

┌─────────────────────────────────────────────────────────────────┐
│                        Eureka 架构                               │
├─────────────────────────────────────────────────────────────────┤
│  Eureka Server 集群                                             │
│  ┌─────────────────┐    ┌─────────────────┐                    │
│  │  Eureka Server  │◄──►│  Eureka Server  │                    │
│  │   (Primary)     │    │   (Secondary)   │                    │
│  │   Zone: us-east │    │   Zone: us-west │                    │
│  └─────────┬───────┘    └─────────┬───────┘                    │
├────────────┼─────────────────────┼────────────────────────────┤
│            │ 注册/续约/获取        │                             │
│            ▼                     ▼                             │
│  ┌─────────────────┐    ┌─────────────────┐                    │
│  │ Eureka Client   │    │ Eureka Client   │                    │
│  │ (Service A)     │    │ (Service B)     │                    │
│  └─────────────────┘    └─────────────────┘                    │
└─────────────────────────────────────────────────────────────────┘

搭建 Eureka Server

1. 创建 Eureka Server 项目

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>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>eureka-server</artifactId>
    <version>1.0.0</version>
    <name>eureka-server</name>
    <description>Eureka Server for Service Discovery</description>
    
    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>2021.0.8</spring-cloud.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. 启动类配置

EurekaServerApplication.java

package com.example.eurekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * Eureka服务注册中心启动类
 * 
 * @EnableEurekaServer 启用Eureka服务端功能
 */
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

3. 配置文件

application.yml

server:
  port: 8761

spring:
  application:
    name: eureka-server
  security:
    user:
      name: admin
      password: admin123
      roles: ADMIN

eureka:
  instance:
    hostname: localhost
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${server.port}
  client:
    # 是否向注册中心注册自己
    register-with-eureka: false
    # 是否从注册中心获取服务列表
    fetch-registry: false
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  server:
    # 关闭自我保护模式(开发环境)
    enable-self-preservation: false
    # 清理间隔(毫秒)
    eviction-interval-timer-in-ms: 5000
    # 响应缓存更新间隔
    response-cache-update-interval-ms: 5000
    # 响应缓存过期时间
    response-cache-auto-expiration-in-seconds: 180

management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

logging:
  level:
    com.netflix.eureka: DEBUG
    com.netflix.discovery: DEBUG

4. 安全配置

SecurityConfig.java

package com.example.eurekaserver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

/**
 * Eureka Server 安全配置
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/actuator/**").permitAll()
                .anyRequest().authenticated()
            )
            .httpBasic();
        
        return http.build();
    }
}

Eureka Server 集群配置

高可用配置

application-peer1.yml

server:
  port: 8761

spring:
  application:
    name: eureka-server
  profiles:
    active: peer1

eureka:
  instance:
    hostname: eureka-peer1
    prefer-ip-address: false
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://admin:admin123@eureka-peer2:8762/eureka/,http://admin:admin123@eureka-peer3:8763/eureka/
  server:
    enable-self-preservation: true
    renewal-percent-threshold: 0.85

application-peer2.yml

server:
  port: 8762

spring:
  application:
    name: eureka-server
  profiles:
    active: peer2

eureka:
  instance:
    hostname: eureka-peer2
    prefer-ip-address: false
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://admin:admin123@eureka-peer1:8761/eureka/,http://admin:admin123@eureka-peer3:8763/eureka/
  server:
    enable-self-preservation: true
    renewal-percent-threshold: 0.85

application-peer3.yml

server:
  port: 8763

spring:
  application:
    name: eureka-server
  profiles:
    active: peer3

eureka:
  instance:
    hostname: eureka-peer3
    prefer-ip-address: false
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://admin:admin123@eureka-peer1:8761/eureka/,http://admin:admin123@eureka-peer2:8762/eureka/
  server:
    enable-self-preservation: true
    renewal-percent-threshold: 0.85

2.3 Eureka Client 服务注册

创建服务提供者

1. 用户服务项目结构

user-service/
├── src/
│   └── main/
│       ├── java/
│       │   └── com/example/userservice/
│       │       ├── UserServiceApplication.java
│       │       ├── controller/
│       │       │   └── UserController.java
│       │       ├── service/
│       │       │   ├── UserService.java
│       │       │   └── impl/
│       │       │       └── UserServiceImpl.java
│       │       ├── entity/
│       │       │   └── User.java
│       │       ├── repository/
│       │       │   └── UserRepository.java
│       │       └── config/
│       │           └── DatabaseConfig.java
│       └── resources/
│           ├── application.yml
│           ├── application-dev.yml
│           └── application-prod.yml
└── pom.xml

2. 依赖配置

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>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>user-service</artifactId>
    <version>1.0.0</version>
    <name>user-service</name>
    <description>User Service for Microservices Demo</description>
    
    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>2021.0.8</spring-cloud.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Starters -->
        <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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <!-- Spring Cloud -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        
        <!-- Database -->
        <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>
        
        <!-- Utilities -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3. 启动类

UserServiceApplication.java

package com.example.userservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * 用户服务启动类
 * 
 * @EnableEurekaClient 启用Eureka客户端功能
 */
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

4. 实体类

User.java

package com.example.userservice.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;

/**
 * 用户实体类
 */
@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
    @Column(unique = true, nullable = false)
    private String username;
    
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    @Column(unique = true, nullable = false)
    private String email;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, message = "密码长度不能少于6个字符")
    @Column(nullable = false)
    private String password;
    
    @Column(name = "full_name")
    private String fullName;
    
    @Column(name = "phone_number")
    private String phoneNumber;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    @Builder.Default
    private UserStatus status = UserStatus.ACTIVE;
    
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }
    
    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
    
    /**
     * 用户状态枚举
     */
    public enum UserStatus {
        ACTIVE,    // 活跃
        INACTIVE,  // 非活跃
        SUSPENDED, // 暂停
        DELETED    // 已删除
    }
}

5. Repository 层

UserRepository.java

package com.example.userservice.repository;

import com.example.userservice.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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.Optional;

/**
 * 用户数据访问层
 */
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    /**
     * 根据用户名查找用户
     */
    Optional<User> findByUsername(String username);
    
    /**
     * 根据邮箱查找用户
     */
    Optional<User> findByEmail(String email);
    
    /**
     * 检查用户名是否存在
     */
    boolean existsByUsername(String username);
    
    /**
     * 检查邮箱是否存在
     */
    boolean existsByEmail(String email);
    
    /**
     * 根据状态查找用户
     */
    Page<User> findByStatus(User.UserStatus status, Pageable pageable);
    
    /**
     * 根据用户名或邮箱模糊查询
     */
    @Query("SELECT u FROM User u WHERE u.username LIKE %:keyword% OR u.email LIKE %:keyword% OR u.fullName LIKE %:keyword%")
    Page<User> findByKeyword(@Param("keyword") String keyword, Pageable pageable);
    
    /**
     * 统计活跃用户数量
     */
    @Query("SELECT COUNT(u) FROM User u WHERE u.status = 'ACTIVE'")
    long countActiveUsers();
}

6. Service 层

UserService.java

package com.example.userservice.service;

import com.example.userservice.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.Optional;

/**
 * 用户服务接口
 */
public interface UserService {
    
    /**
     * 创建用户
     */
    User createUser(User user);
    
    /**
     * 根据ID获取用户
     */
    Optional<User> getUserById(Long id);
    
    /**
     * 根据用户名获取用户
     */
    Optional<User> getUserByUsername(String username);
    
    /**
     * 根据邮箱获取用户
     */
    Optional<User> getUserByEmail(String email);
    
    /**
     * 更新用户信息
     */
    User updateUser(Long id, User user);
    
    /**
     * 删除用户
     */
    void deleteUser(Long id);
    
    /**
     * 分页查询用户
     */
    Page<User> getUsers(Pageable pageable);
    
    /**
     * 根据关键字搜索用户
     */
    Page<User> searchUsers(String keyword, Pageable pageable);
    
    /**
     * 检查用户名是否存在
     */
    boolean existsByUsername(String username);
    
    /**
     * 检查邮箱是否存在
     */
    boolean existsByEmail(String email);
    
    /**
     * 获取活跃用户数量
     */
    long getActiveUserCount();
}

UserServiceImpl.java

package com.example.userservice.service.impl;

import com.example.userservice.entity.User;
import com.example.userservice.repository.UserRepository;
import com.example.userservice.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

/**
 * 用户服务实现类
 */
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class UserServiceImpl implements UserService {
    
    private final UserRepository userRepository;
    private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    
    @Override
    public User createUser(User user) {
        log.info("Creating user: {}", user.getUsername());
        
        // 检查用户名和邮箱是否已存在
        if (existsByUsername(user.getUsername())) {
            throw new IllegalArgumentException("用户名已存在: " + user.getUsername());
        }
        
        if (existsByEmail(user.getEmail())) {
            throw new IllegalArgumentException("邮箱已存在: " + user.getEmail());
        }
        
        // 加密密码
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        
        User savedUser = userRepository.save(user);
        log.info("User created successfully: {}", savedUser.getId());
        
        return savedUser;
    }
    
    @Override
    @Transactional(readOnly = true)
    public Optional<User> getUserById(Long id) {
        log.debug("Getting user by id: {}", id);
        return userRepository.findById(id);
    }
    
    @Override
    @Transactional(readOnly = true)
    public Optional<User> getUserByUsername(String username) {
        log.debug("Getting user by username: {}", username);
        return userRepository.findByUsername(username);
    }
    
    @Override
    @Transactional(readOnly = true)
    public Optional<User> getUserByEmail(String email) {
        log.debug("Getting user by email: {}", email);
        return userRepository.findByEmail(email);
    }
    
    @Override
    public User updateUser(Long id, User user) {
        log.info("Updating user: {}", id);
        
        User existingUser = userRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("用户不存在: " + id));
        
        // 更新用户信息
        if (user.getFullName() != null) {
            existingUser.setFullName(user.getFullName());
        }
        if (user.getPhoneNumber() != null) {
            existingUser.setPhoneNumber(user.getPhoneNumber());
        }
        if (user.getStatus() != null) {
            existingUser.setStatus(user.getStatus());
        }
        
        User updatedUser = userRepository.save(existingUser);
        log.info("User updated successfully: {}", updatedUser.getId());
        
        return updatedUser;
    }
    
    @Override
    public void deleteUser(Long id) {
        log.info("Deleting user: {}", id);
        
        User user = userRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("用户不存在: " + id));
        
        // 软删除:更新状态为已删除
        user.setStatus(User.UserStatus.DELETED);
        userRepository.save(user);
        
        log.info("User deleted successfully: {}", id);
    }
    
    @Override
    @Transactional(readOnly = true)
    public Page<User> getUsers(Pageable pageable) {
        log.debug("Getting users with pagination: {}", pageable);
        return userRepository.findAll(pageable);
    }
    
    @Override
    @Transactional(readOnly = true)
    public Page<User> searchUsers(String keyword, Pageable pageable) {
        log.debug("Searching users with keyword: {}", keyword);
        return userRepository.findByKeyword(keyword, pageable);
    }
    
    @Override
    @Transactional(readOnly = true)
    public boolean existsByUsername(String username) {
        return userRepository.existsByUsername(username);
    }
    
    @Override
    @Transactional(readOnly = true)
    public boolean existsByEmail(String email) {
        return userRepository.existsByEmail(email);
    }
    
    @Override
    @Transactional(readOnly = true)
    public long getActiveUserCount() {
        return userRepository.countActiveUsers();
    }
}

7. Controller 层

UserController.java

package com.example.userservice.controller;

import com.example.userservice.entity.User;
import com.example.userservice.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import java.util.HashMap;
import java.util.Map;

/**
 * 用户控制器
 */
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Slf4j
@Validated
@RefreshScope
public class UserController {
    
    private final UserService userService;
    
    @Value("${server.port}")
    private String serverPort;
    
    @Value("${spring.application.name}")
    private String applicationName;
    
    /**
     * 健康检查接口
     */
    @GetMapping("/health")
    public ResponseEntity<Map<String, Object>> health() {
        Map<String, Object> response = new HashMap<>();
        response.put("status", "UP");
        response.put("service", applicationName);
        response.put("port", serverPort);
        response.put("timestamp", System.currentTimeMillis());
        response.put("activeUsers", userService.getActiveUserCount());
        
        return ResponseEntity.ok(response);
    }
    
    /**
     * 创建用户
     */
    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        log.info("Creating user: {}", user.getUsername());
        
        try {
            User createdUser = userService.createUser(user);
            return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
        } catch (IllegalArgumentException e) {
            log.error("Error creating user: {}", e.getMessage());
            return ResponseEntity.badRequest().build();
        }
    }
    
    /**
     * 根据ID获取用户
     */
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable @Min(1) Long id) {
        log.debug("Getting user by id: {}", id);
        
        return userService.getUserById(id)
            .map(user -> ResponseEntity.ok().body(user))
            .orElse(ResponseEntity.notFound().build());
    }
    
    /**
     * 根据用户名获取用户
     */
    @GetMapping("/username/{username}")
    public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
        log.debug("Getting user by username: {}", username);
        
        return userService.getUserByUsername(username)
            .map(user -> ResponseEntity.ok().body(user))
            .orElse(ResponseEntity.notFound().build());
    }
    
    /**
     * 分页查询用户
     */
    @GetMapping
    public ResponseEntity<Page<User>> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy,
            @RequestParam(defaultValue = "asc") String sortDir,
            @RequestParam(required = false) String keyword) {
        
        log.debug("Getting users - page: {}, size: {}, sortBy: {}, sortDir: {}, keyword: {}", 
                 page, size, sortBy, sortDir, keyword);
        
        Sort sort = sortDir.equalsIgnoreCase("desc") ? 
                   Sort.by(sortBy).descending() : 
                   Sort.by(sortBy).ascending();
        
        Pageable pageable = PageRequest.of(page, size, sort);
        
        Page<User> users = (keyword != null && !keyword.trim().isEmpty()) ?
                          userService.searchUsers(keyword, pageable) :
                          userService.getUsers(pageable);
        
        return ResponseEntity.ok(users);
    }
    
    /**
     * 更新用户
     */
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable @Min(1) Long id, 
            @Valid @RequestBody User user) {
        
        log.info("Updating user: {}", id);
        
        try {
            User updatedUser = userService.updateUser(id, user);
            return ResponseEntity.ok(updatedUser);
        } catch (IllegalArgumentException e) {
            log.error("Error updating user: {}", e.getMessage());
            return ResponseEntity.notFound().build();
        }
    }
    
    /**
     * 删除用户
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable @Min(1) Long id) {
        log.info("Deleting user: {}", id);
        
        try {
            userService.deleteUser(id);
            return ResponseEntity.noContent().build();
        } catch (IllegalArgumentException e) {
            log.error("Error deleting user: {}", e.getMessage());
            return ResponseEntity.notFound().build();
        }
    }
    
    /**
     * 检查用户名是否存在
     */
    @GetMapping("/check/username/{username}")
    public ResponseEntity<Map<String, Boolean>> checkUsername(@PathVariable String username) {
        boolean exists = userService.existsByUsername(username);
        Map<String, Boolean> response = new HashMap<>();
        response.put("exists", exists);
        return ResponseEntity.ok(response);
    }
    
    /**
     * 检查邮箱是否存在
     */
    @GetMapping("/check/email/{email}")
    public ResponseEntity<Map<String, Boolean>> checkEmail(@PathVariable String email) {
        boolean exists = userService.existsByEmail(email);
        Map<String, Boolean> response = new HashMap<>();
        response.put("exists", exists);
        return ResponseEntity.ok(response);
    }
    
    /**
     * 获取用户统计信息
     */
    @GetMapping("/stats")
    public ResponseEntity<Map<String, Object>> getUserStats() {
        Map<String, Object> stats = new HashMap<>();
        stats.put("activeUsers", userService.getActiveUserCount());
        stats.put("service", applicationName);
        stats.put("port", serverPort);
        
        return ResponseEntity.ok(stats);
    }
}

8. 配置文件

application.yml

server:
  port: 8081

spring:
  application:
    name: user-service
  profiles:
    active: dev
  
  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/microservice_user?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
    
  # JPA配置
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
        format_sql: true
    
  # Jackson配置
  jackson:
    default-property-inclusion: non_null
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai

# Eureka客户端配置
eureka:
  client:
    service-url:
      defaultZone: http://admin:admin123@localhost:8761/eureka/
    register-with-eureka: true
    fetch-registry: true
    # 从注册中心获取服务列表的间隔时间
    registry-fetch-interval-seconds: 30
  instance:
    # 使用IP地址注册
    prefer-ip-address: true
    # 实例ID
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}
    # 心跳间隔
    lease-renewal-interval-in-seconds: 30
    # 服务失效时间
    lease-expiration-duration-in-seconds: 90
    # 健康检查路径
    health-check-url-path: /actuator/health
    # 状态页面路径
    status-page-url-path: /actuator/info
    # 元数据
    metadata-map:
      version: 1.0.0
      description: User management service

# Actuator配置
management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always
  info:
    env:
      enabled: true

# 应用信息
info:
  app:
    name: ${spring.application.name}
    description: User Service for Microservices Architecture
    version: 1.0.0
  build:
    artifact: user-service
    group: com.example

# 日志配置
logging:
  level:
    com.example.userservice: DEBUG
    org.springframework.cloud: DEBUG
    com.netflix.eureka: INFO
    com.netflix.discovery: INFO
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{50}] - %msg%n"

application-dev.yml

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password: 
  
  h2:
    console:
      enabled: true
      path: /h2-console
  
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true

eureka:
  client:
    service-url:
      defaultZone: http://admin:admin123@localhost:8761/eureka/

logging:
  level:
    root: INFO
    com.example.userservice: DEBUG

application-prod.yml

spring:
  datasource:
    url: jdbc:mysql://mysql-server:3306/microservice_user?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
    username: ${DB_USERNAME:user_service}
    password: ${DB_PASSWORD:password}
    
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

eureka:
  client:
    service-url:
      defaultZone: http://admin:admin123@eureka-server:8761/eureka/
  instance:
    prefer-ip-address: false
    hostname: user-service

logging:
  level:
    root: WARN
    com.example.userservice: INFO
  file:
    name: /var/log/user-service.log

2.4 服务发现与调用

使用 RestTemplate 进行服务调用

配置 RestTemplate

RestTemplateConfig.java

package com.example.orderservice.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * RestTemplate配置类
 */
@Configuration
public class RestTemplateConfig {
    
    /**
     * 创建负载均衡的RestTemplate
     * @LoadBalanced 注解启用客户端负载均衡
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

服务调用示例

OrderService.java

package com.example.orderservice.service.impl;

import com.example.orderservice.entity.Order;
import com.example.orderservice.service.OrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
 * 订单服务实现类
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderServiceImpl implements OrderService {
    
    private final RestTemplate restTemplate;
    
    @Override
    public Order createOrder(CreateOrderRequest request) {
        log.info("Creating order for user: {}", request.getUserId());
        
        // 调用用户服务获取用户信息
        String userServiceUrl = "http://user-service/api/users/" + request.getUserId();
        
        try {
            User user = restTemplate.getForObject(userServiceUrl, User.class);
            
            if (user == null) {
                throw new IllegalArgumentException("用户不存在: " + request.getUserId());
            }
            
            // 创建订单
            Order order = Order.builder()
                .userId(user.getId())
                .userName(user.getUsername())
                .userEmail(user.getEmail())
                .totalAmount(request.getTotalAmount())
                .status(Order.OrderStatus.PENDING)
                .build();
            
            // 保存订单
            Order savedOrder = orderRepository.save(order);
            
            log.info("Order created successfully: {}", savedOrder.getId());
            return savedOrder;
            
        } catch (Exception e) {
            log.error("Error calling user service: {}", e.getMessage());
            throw new RuntimeException("创建订单失败: " + e.getMessage());
        }
    }
}

使用 DiscoveryClient 进行服务发现

ServiceDiscoveryController.java

package com.example.orderservice.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * 服务发现控制器
 */
@RestController
@RequestMapping("/api/discovery")
@RequiredArgsConstructor
public class ServiceDiscoveryController {
    
    private final DiscoveryClient discoveryClient;
    
    /**
     * 获取所有服务列表
     */
    @GetMapping("/services")
    public List<String> getServices() {
        return discoveryClient.getServices();
    }
    
    /**
     * 获取指定服务的实例列表
     */
    @GetMapping("/services/{serviceName}/instances")
    public List<ServiceInstance> getServiceInstances(@PathVariable String serviceName) {
        return discoveryClient.getInstances(serviceName);
    }
    
    /**
     * 获取服务实例详细信息
     */
    @GetMapping("/services/{serviceName}/info")
    public ServiceInstance getServiceInfo(@PathVariable String serviceName) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
        
        if (instances.isEmpty()) {
            throw new RuntimeException("Service not found: " + serviceName);
        }
        
        // 返回第一个实例
        return instances.get(0);
    }
}

2.5 总结

本章详细介绍了Spring Cloud中的服务注册与发现机制:

核心概念

  1. 服务注册中心:集中管理服务实例信息
  2. 服务提供者:注册服务并提供服务能力
  3. 服务消费者:发现并调用其他服务

Eureka 特点

  1. AP模式:保证可用性和分区容错性
  2. 自我保护机制:防止网络分区时误删服务实例
  3. 客户端缓存:提高服务发现性能
  4. 集群支持:支持多节点部署提高可用性

最佳实践

  1. 健康检查:配置合适的健康检查路径
  2. 实例元数据:添加有用的实例元数据信息
  3. 负载均衡:使用@LoadBalanced注解启用客户端负载均衡
  4. 异常处理:妥善处理服务调用异常
  5. 监控告警:监控服务注册状态和调用情况

在下一章中,我们将学习如何使用OpenFeign进行声明式服务调用,以及Ribbon负载均衡的配置和使用。