启动Consul服务, 在Win10下可以执行以下命令, 或者存成bat文件运行, 保持窗口打开
consul agent -dev -client=0.0.0.0 -data-dir .\ -advertise 127.0.0.1 -ui -config-dir .\
浏览器访问 http://127.0.0.1:8500 , 用于观察后面注册的Node和Health情况
这个演示项目使用的 Spring Boot 和 Spring Cloud 都不是最新版本, 因为最新版本最低要求 JDK17. 这里选择的是对应 JDK11 可用的最高版本, 各组件版本明细为
这个用于演示的项目名称为 Dummy, 包含3个子模块, 分别是 dummy-common-api, dummy-common-impl 和 dummy-admin, 其中
打包后, 需要部署的是两个jar: dummy-common.jar 和 dummy-admin.jar, 前者提供服务接口, 后者消费前者提供的接口, 并对外(例如前端, 小程序, APP)提供接口
项目的整体结构如下
│ pom.xml ├───dummy-admin │ │ pom.xml │ ├───src │ │ ├───main │ │ │ ├───java │ │ │ └───resources │ │ │ application.yml │ │ └───test │ └───target ├───dummy-common-api │ │ pom.xml │ ├───src │ │ ├───main │ │ │ ├───java │ │ │ └───resources │ │ └───test │ └───target └───dummy-common-impl │ pom.xml ├───src │ ├───main │ │ ├───java │ │ └───resources │ │ application.yml │ └───test └───target
根模块的 pom.xml 中,
<?xml version="1.0" encoding="UTF-8"?> ... <name>Dummy: Root</name> <modules> <module>dummy-common-api</module> <module>dummy-common-impl</module> <module>dummy-admin</module> </modules> <properties> <!-- Global encoding --> <project.jdk.version>11</project.jdk.version> <project.source.encoding>UTF-8</project.source.encoding> <!-- Global dependency versions --> <spring-boot.version>2.7.11</spring-boot.version> <spring-cloud.version>2021.0.6</spring-cloud.version> </properties> <dependencyManagement> <dependencies> <!-- Spring Boot Dependencies --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- Spring Cloud 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> ... </build> </project>
这个模块用于生成依赖的jar包, 作用非常重要. 以下详细说明
pom.xml 中除了定义和父模块的关系, 需要引入 openfeign
<?xml version="1.0" encoding="UTF-8"?> ... <parent> <groupId>com.rockbb.test</groupId> <artifactId>dummy</artifactId> <version>1.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <artifactId>dummy-common-api</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>Dummy: Commons API</name> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> ... </dependencies> <build> ... </build> </project>
定义一个 UserDTO, 这个是用于传输的数据对象
@Data public class UserDTO implements Serializable { private Long id; private String name; }
对应的服务接口. 这里用到了 @FeignClient 注解
@FeignClient(name = CommonConstant.SERVICE_NAME, contextId = "userDTOService", path = "/userDTOService") public interface UserDTOService { @GetMapping("/get") UserDTO get(@RequestParam("id") long id); @PostMapping("/add") int add(@RequestBody UserDTO dto); }
在 dummy-admin 中, 这个接口会被实例化为 feign 代理, 在模块中可以像普通 service 一样调用, 而在 dummy-common 中, 不引入 feign 依赖, 或者在 @EnableFeignClients 的 basePackages 中避开本包路径, 就会忽略这个注解, 从而实现模块间接口的关联.
与现在很多 Spring Cloud 项目中单独拆出一个 Service 模块的做法, 这种实现有很多的优点
模块的 pom.xml
<?xml version="1.0" encoding="UTF-8"?> ... <name>Dummy: Common Implementation</name> <dependencies> <!-- Spring Boot Dependencies --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Spring Cloud Dependencies consul-discovery 和 consul-all 二选一 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-all</artifactId> </dependency> ... <dependency> <groupId>com.rockbb.test</groupId> <artifactId>dummy-common-api</artifactId> <version>${project.version}</version> </dependency> </dependencies> <build> <finalName>dummy-common</finalName> <resources> ... </resources> <plugins> ... <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
配置部分 application.yml
server: port: 8762 tomcat: uri-encoding: UTF-8 servlet: context-path: / spring: application: name: dummy-common config: import: 'optional:consul:' #This will connect to the Consul Agent at the default location of "http://localhost:8500" # cloud: # consul: # host: 127.0.0.1 # port: 8500 # discovery: # health-check-path: /health # replace the default /actuator/health # instance-id: ${spring.application.name}:${random.value}
代码部分, 首先是实现 health 检查的处理方法, 这部分是普通的 RestController 方法. 返回字符串可以任意指定, 只要返回的 code 是 200 就可以
@RestController public class HealthCheckServiceImpl { @GetMapping("/health") public String get() { return "SUCCESS"; } }
服务接口的实现类, 这里实现了两个接口方法 get 和 add
@RestController @RequestMapping("userDTOService") public class UserDTOServiceImpl implements UserDTOService { @Autowired private UserRepo userRepo; @Override public UserDTO get(long id) { log.debug("Get user: {}", id); UserDTO user = new UserDTO(); user.setId(id); user.setName("dummy"); return user; } @Override public int add(UserDTO dto) { log.debug("Add user: {}", dto.getName()); return 0; } }
dummy-common 模块运行后会将接口注册到 Consul, 启动后注意观察两部分:
dummy-admin 是调用接口, 并对外提供服务的模块
pom.xml 和 dummy-common 基本一样, 因为都要连接 Consul, 都要提供 Controller 方法
<?xml version="1.0" encoding="UTF-8"?> ... <name>Dummy: Admin API</name> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <dependency> <groupId>com.rockbb.test</groupId> <artifactId>dummy-common-api</artifactId> <version>${project.version}</version> </dependency> </dependencies> <build> <finalName>dummy-admin</finalName> <resources> ... </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> ... </plugins> </build> </project>
在主应用入口, 除了 @SpringBootApplication 以外, 还需要增加两个注解
/* Attach to discovery service without registering itself */ @EnableDiscoveryClient(autoRegister=false) @EnableFeignClients(basePackages = {"com.rockbb.test.dummy.common.api"}) @SpringBootApplication public class AdminApp { public static void main(String[] args) { SpringApplication.run(AdminApp.class, args); } }
在调用方法的地方, 按普通 Service 注入和调用
@Slf4j @RestController public class IndexController { @Autowired private UserDTOService userDTOService; @GetMapping(value = "/user_get") public String doGetUser() { UserDTO user = userDTOService.get(100L); return user.getId() + ":" + user.getName(); } @GetMapping(value = "/user_add") public String doAddUser() { UserDTO user = new UserDTO(); user.setName("foobar"); int result = userDTOService.add(user); return String.valueOf(result); }
可以通过注入的 DiscoveryClient 对象, 查看对应服务的服务地址(一般不需要)
@Autowired private DiscoveryClient discoveryClient; @GetMapping("/services") public Optional<URI> serviceURL() { return discoveryClient.getInstances(CommonConstant.SERVICE_NAME) .stream() .map(ServiceInstance::getUri) .findFirst(); }