为了后续顺利学习 Spring Cloud OpenFeign,下面将介绍如何准备学习环境,该环境主要包含两个服务,一个注册中心 Eureka,另一个示例服务 service-demo,该服务将提供简单的 GET、POST、PUT 等简单服务,用于 Spring Cloud OpenFeign 调用。项目结构如下图:
本教程采用 JDK17,如下图:
Eureka 是 Netflix 开源的一个服务发现框架,主要用于在分布式系统中实现服务注册和发现的功能(点击快速学习)。项目结构如下图:
上述项目中,仅有一个 application.yml 配置,和一个启动类 NetflixEurekaServerApplication。
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 https://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>3.2.9</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.hxstrive</groupId> <artifactId>openfeign-eureka-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>netflix-eureka-server</name> <description>netflix-eureka-server</description> <properties> <java.version>17</java.version> <spring-cloud.version>2023.0.3</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <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> <finalName>netflix-eureka-server</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.yml 配置内容如下:
# 默认端口 server: port: 8761 # 应用名称 spring: application: name: eureka-server # Eureka 配置 eureka: # 指定 Eureka 服务实例自身的主机名 instance: hostname: localhost client: register-with-eureka: false fetch-registry: false server: enable-self-preservation: true renewal-percent-threshold: 0.5
下面是一个简单的 Spring Boot 启动类,该类上额外添加了 @EnableEurekaServer 注解,用于标记该服务是一个 Eureka 服务器,代码如下:
package com.hxstrive.netflixeurekaserver; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; /** * 启动 Eureka Server 注册中心 * @author hxstrive */ @SpringBootApplication @EnableEurekaServer public class OpenfeignEurekaServerApplication { public static void main(String[] args) { SpringApplication.run(OpenfeignEurekaServerApplication.class, args); } }
service-demo 服务提供简单的 GET、POST、PUT 等简单服务,用于 Spring Cloud OpenFeign 调用。
项目结构如下图:
包说明:
entity 存放实体,仅有一个 User 实体
dto 数据传输对象,其中 CommonReturn 定义了接口通用传输格式
handler 定义了全局异常处理器
exception 定义业务自定义异常类
controller 定义三个业务 Controller,分别用于简单 POST、GET 服务,用户信息管理 Controller
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 https://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>3.2.9</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.hxstrive</groupId> <artifactId>openfeign-service-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>service-demo</name> <description>service-demo</description> <properties> <java.version>17</java.version> <spring-cloud.version>2023.0.3</spring-cloud.version> <base.path>${project.basedir}</base.path> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- spring cloud 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- 文件上传 --> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </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> <finalName>service-demo</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.yml 配置内容如下:
server: port: 8090 spring: application: name: service-demo ## 服务地址 eureka: client: enabled: true service-url: # 注册中心路径,表示我们向这个注册中心注册服务,如果向多个注册中心注册,用“,”进行分隔 defaultZone: http://localhost:8761/eureka instance: hostname: localhost # 心跳间隔5s,默认30s。每一个服务配置后,心跳间隔和心跳超时时间会被保存在server端, # 不同服务的心跳频率可能不同,server端会根据保存的配置来分别探活 lease-renewal-interval-in-seconds: 5 # 心跳超时时间10s,默认90s。从client端最后一次发出心跳后, # 达到这个时间没有再次发出心跳,表示服务不可用,将它的实例从注册中心移除 lease-expiration-duration-in-seconds: 10
package com.hxstrive.service_demo.dto; import lombok.Data; /** * 通用返回对象 * @author hxstrive.com */ @Data public class CommonReturn<T> { private int code; private String message; private T data; private String appName; private String port; public static <T> CommonReturn<T> success(T data) { CommonReturn<T> commonReturn = new CommonReturn<>(); commonReturn.setCode(1); commonReturn.setData(data); return commonReturn; } public static <T> CommonReturn<T> success() { return success(null); } public static <T> CommonReturn<T> fail(String message) { CommonReturn<T> commonReturn = new CommonReturn<>(); commonReturn.setCode(0); commonReturn.setMessage(message); return commonReturn; } public CommonReturn<T> ext(String appName, String port) { this.setAppName(appName); this.setPort(port); return this; } }
package com.hxstrive.service_demo.entity; import lombok.Builder; import lombok.Data; import lombok.ToString; /** * 用户实体 * @author hxstrive.com */ @Data @Builder @ToString public class User { private Long id; private String name; private Integer age; }
package com.hxstrive.service_demo.exception; /** * 业务异常 * @author hxstrive.com */ public class BusinessException extends RuntimeException { public BusinessException() { } public BusinessException(String message) { super(message); } public BusinessException(String message, Throwable cause) { super(message, cause); } public BusinessException(Throwable cause) { super(cause); } }
package com.hxstrive.service_demo.handler; import com.hxstrive.service_demo.dto.CommonReturn; import com.hxstrive.service_demo.exception.BusinessException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; /** * 全局异常处理 * @author hxstrive.com * @since 1.0.0 2024/10/23 9:28 */ @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) @ResponseBody public CommonReturn<?> handleBusinessException(BusinessException e) { return CommonReturn.fail(e.getMessage()); } @ExceptionHandler(Exception.class) @ResponseBody public CommonReturn<?> handleException(Exception e) { return CommonReturn.fail("服务异常,稍后再试"); } }
package com.hxstrive.service_demo.controller; import com.hxstrive.service_demo.entity.User; import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import java.util.Map; import java.util.UUID; /** * 简单控制器 * @author hxstrive.com */ @RestController @RequestMapping("/simple") public class SimpleController { @Value("${spring.application.name}") private String appName; @Value("${server.port}") private String appPort; @GetMapping("/hello") public String hello(HttpServletRequest request) { return "Hello World"; } @GetMapping("/hello2") public String hello2(@RequestParam("args") String[] args) { return "Hello World, args=" + String.join("、", args); } @GetMapping("/info") public String info() { return "appName=" + appName + " appPort=" + appPort + " uuid=" + UUID.randomUUID().toString(); } @GetMapping("/get") public String get(@RequestParam("id") Long id) { return "appName=" + appName + " appPort=" + appPort + " id=" + id; } @GetMapping("/param1") public String param1(@RequestParam("token") String token) { return "token=" + token; } @GetMapping("/param2") public String param2(@RequestHeader("Authorization") String token) { return "Authorization=" + token; } @PostMapping("/param3") public String param3(@RequestBody String body) { return "body=" + body; } @GetMapping("/header1") public String header1(@RequestHeader("Custom-Header") String customHeader, @RequestHeader("Authorization") String token, @RequestHeader("Content-Type") String contentType) { return "Custom-Header=" + customHeader + "<br/>Authorization=" + token + "<br/>Content-Type=" + contentType; } @PostMapping("/body1") public Map<String,String> body1(@RequestBody Map<String,String> body) { body.put("service_name", "ServiceDemo"); return body; } @PostMapping("/body2") public User body2(@RequestBody User user) { return user; } @GetMapping("/query") public User query(@RequestParam("id") Long id, @RequestParam("name") String name, @RequestParam("age") Integer age) { return User.builder().id(id).name(name).age(age).build(); } @GetMapping("/interceptor") public String interceptor(@RequestHeader("X-Forwarded-For") String xForwardedFor, @RequestParam("msg") String msg) { return "xForwardedFor=" + xForwardedFor + "<br/>msg=" + msg; } @PostMapping("/encode") public User encode(@RequestBody User user) { System.out.println(user); return user; } }
package com.hxstrive.service_demo.controller; import com.hxstrive.service_demo.dto.CommonReturn; import com.hxstrive.service_demo.entity.User; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.List; /** * 用户控制器 * @author hxstrive.com */ @Slf4j @RestController @RequestMapping("/user") public class UserController { private final static List<User> USERS = new ArrayList<>(); static { USERS.add(User.builder().id(1L).name("Tom").age(20).build()); USERS.add(User.builder().id(2L).name("Helen").age(30).build()); USERS.add(User.builder().id(3L).name("Bill").age(40).build()); } @Value("${spring.application.name}") private String appName; @Value("${server.port}") private String appPort; // GET 请求,获取所有用户信息 @GetMapping("/getAllUsers") public CommonReturn<List<User>> getAllUsers() { log.info("getAllUsers()"); return CommonReturn.success(USERS).ext(appName, appPort); } // GET 请求,根据用户 ID 获取用户信息 @GetMapping("/getUserById") public CommonReturn<User> getUserById(@RequestParam("id") Long id) { log.info("getUserById() id={}", id); return USERS.stream() .filter(user -> user.getId().equals(id)) .findFirst() .map(u -> CommonReturn.success(u).ext(appName, appPort)) .orElse(CommonReturn.fail("用户不存在")); } // POST 请求,创建新用户 @PostMapping("/createUser") public CommonReturn<User> createUser(@RequestBody User user) { log.info("createUser() user={}", user); USERS.add(user); return CommonReturn.success(user).ext(appName, appPort); } // PUT 请求,更新用户信息 @PutMapping("/updateUser") public CommonReturn<User> updateUser(@RequestParam("id") Long id, @RequestBody User updatedUser) { log.info("updateUser() id={}, updateUser={}", id, updatedUser); for (int i = 0; i < USERS.size(); i++) { if (USERS.get(i).getId().equals(id)) { USERS.set(i, updatedUser); return CommonReturn.success(updatedUser).ext(appName, appPort); } } return CommonReturn.fail("更新用户信息失败"); } // DELETE 请求,删除用户 @DeleteMapping("/deleteUser") public CommonReturn<String> deleteUser(@RequestParam("id") Long id) { log.info("deleteUser() id={}", id); USERS.removeIf(user -> user.getId().equals(id)); return CommonReturn.success("删除成功").ext(appName, appPort); } }
package com.hxstrive.service_demo.controller; import com.hxstrive.service_demo.dto.CommonReturn; import com.hxstrive.service_demo.entity.User; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.List; /** * Restful 风格的用户控制器 * @author hxstrive.com */ @Slf4j @RestController @RequestMapping("/user/restful") public class UserRestfulController { private final static List<User> USERS = new ArrayList<>(); static { USERS.add(User.builder().id(1L).name("Tom").age(20).build()); USERS.add(User.builder().id(2L).name("Helen").age(30).build()); USERS.add(User.builder().id(3L).name("Bill").age(40).build()); } @Value("${spring.application.name}") private String appName; @Value("${server.port}") private String appPort; // GET 请求,获取所有用户信息 @GetMapping("/getAllUsers") public CommonReturn<List<User>> getAllUsers() { log.info("getAllUsers()"); return CommonReturn.success(USERS).ext(appName, appPort); } // GET 请求,根据用户 ID 获取用户信息 @GetMapping("/{id}") public CommonReturn<User> getUserById(@PathVariable Long id) { log.info("getUserById() id={}", id); return USERS.stream() .filter(user -> user.getId().equals(id)) .findFirst() .map((u) -> CommonReturn.success(u).ext(appName, appPort)) .orElse(CommonReturn.fail("用户不存在")); } // POST 请求,创建新用户 @PostMapping("/createUser") public CommonReturn<User> createUser(@RequestBody User user) { log.info("createUser() user={}", user); USERS.add(user); return CommonReturn.success(user).ext(appName, appPort); } // PUT 请求,更新用户信息 @PutMapping("/{id}") public CommonReturn<User> updateUser(@PathVariable Long id, @RequestBody User updatedUser) { log.info("updateUser() id={}, updateUser={}", id, updatedUser); for (int i = 0; i < USERS.size(); i++) { if (USERS.get(i).getId().equals(id)) { USERS.set(i, updatedUser); return CommonReturn.success(updatedUser).ext(appName, appPort); } } return CommonReturn.fail("更新用户信息失败"); } // DELETE 请求,删除用户 @DeleteMapping("/{id}") public CommonReturn<String> deleteUser(@PathVariable Long id) { log.info("deleteUser() id={}", id); USERS.removeIf(user -> user.getId().equals(id)); return CommonReturn.success("删除成功"); } }
package com.hxstrive.service_demo; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * 入口 * @author Administrator */ @RestController @SpringBootApplication public class OpenfeignServiceDemoApplication { @Value("${spring.application.name}") private String appName; @Value("${server.port}") private String appPort; public static void main(String[] args) { SpringApplication.run(OpenfeignServiceDemoApplication.class, args); } @GetMapping("/") public String index() { return "appName=" + appName + ", appPort=" + appPort; } @GetMapping("/hello") public String hello() { return "Hello World! appName=" + appName + ", appPort=" + appPort + "!"; } }
先启动前面创建的 Eureka 服务端,然后启动 service-demo 服务,成功启动后,状态如下图:
访问 http://localhost:8761 地址,查看 Eureka 管理页面,如下图:
如果能够正常看见该页面,且存在 SERVICE-DEMO 服务,则环境准备成功了。
此时,可以进行后续 Spring Cloud OpenFeign 的学习了……