单体项目改造微服务
ps:将一个 springboot 项目改造为微服务项目所需要的详细步骤。
技术栈:
- SpringBoot 2.6.13
- Spring Cloud Alibaba 2021.0.5.0
- Nacos
- OpenFeign
- Knife4j
注意!!!版本一定要对应
spring cloud | spring boot |
---|---|
2023.0.2 ,2023.0.3 | 3.3.x ,3.2.x |
2021.0.5 | 2.6.13 |
服务划分
- 依赖模块:
- 注册中心:Nacos
- 微服务网关 gateway:Gateway 聚合所有的接口,统一接受处理前端的请求
- 公共模块:
- common模块:全局异常处理器、请求响应封装类、公用的工具类等
- model 模型模块:实体类
- service-client 公共接口模块(OpenFeign):只存放接口,不存放实现(多个服务之间要共享)
- 业务模块:
- 用户服务(端口:8101)
- 题目服务(端口:8102)
- … …
路由划分
用 springboot 的 context-path 统一修改各项目的接口前缀,比如:
用户服务:
- /api/user
- /api/user/inner(内部调用,网关层面要做限制)
题目服务:
- /api/question(也包括题目提交信息)
- /api/question/inner(内部调用,网关层面要做限制)
Nacos 启动
安装后,进入bin
目录执行命令启动:
linux:
sh startup.sh -m standalone
ubuntu:
bash startup.sh -m standalone
windows:
startup.cmd -m standalone
新建项目
创建微服务项目
推荐:
补充:(待定)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>xxx</version>
<type>pom</type>
<scope>import</scope>
</dependency>
- 依次使用 new modules 和 spring boot Initializr 创建各模块
- 给各模块之间绑定子父依赖关系
同步代码和依赖
- 公共服务:
- 无脑搬运
业务服务:
- 无脑搬运(原 controller,service)
- 引入公共模块(model 服务,commone 服务,service-client 服务)
service-client 公共接口服务:
- 无脑搬运所有的 service 接口
- 引入 OpenFeign 依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>x.x.x</version> </dependency>
启动类的注解:(业务服务+网关,公共接口服务不需要启动类)
@SpringBootApplication
@MapperScan("com.yannqing.yanojbackendquestionservice.mapper")
@EnableScheduling
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@ComponentScan("com.yannqing")
服务内部调用
Open Feign:Http 调用客户端,提供了更方便的方式来让你远程调用其他服务,不用关心服务的调用地址
- 解决找不到 Bean 的问题
- 梳理调用关系:(什么服务依赖了什么服务,要体现到具体代码)
- 确认要提供哪些服务
- 实现 client 方法
- 实现较为简单直接使用默认实现,通过
default
关键字 - 其余的,在业务服务的 controller 中,新建 inner 包,表示内部调用服务。实现对应 feignClient 接口即可
- 实现较为简单直接使用默认实现,通过
示例代码:
/**
* 用户服务
*
*/
@FeignClient(name = "yuoj-backend-user-service", path = "/api/user/inner")
public interface UserFeignClient {
/**
* 根据 id 获取用户
* @param userId
* @return
*/
@GetMapping("/get/id")
User getById(@RequestParam("userId") long userId);
/**
* 根据 id 获取用户列表
* @param idList
* @return
*/
@GetMapping("/get/ids")
List<User> listByIds(@RequestParam("idList") Collection<Long> idList);
/**
* 获取当前登录用户
*
* @param request
* @return
*/
default User getLoginUser(HttpServletRequest request) {
// 先判断是否已登录
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User currentUser = (User) userObj;
if (currentUser == null || currentUser.getId() == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
}
return currentUser;
}
/**
* 是否为管理员
*
* @param user
* @return
*/
default boolean isAdmin(User user) {
return user != null && UserRoleEnum.ADMIN.getValue().equals(user.getUserRole());
}
/**
* 获取脱敏的用户信息
*
* @param user
* @return
*/
default UserVO getUserVO(User user) {
if (user == null) {
return null;
}
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
return userVO;
}
}
- 修改各业务服务的调用代码为 feignClient,(例如:UserService -> UserFeignClient)
- 编写 feignClient 服务的实现类,注意要和之前定义的客户端保持一致
@RestController
@RequestMapping("/inner")
public class UserInnerController implements UserFeignClient {
@Resource
private UserService userService;
/**
* 根据 id 获取用户
* @param userId
* @return
*/
@Override
@GetMapping("/get/id")
public User getById(@RequestParam("userId") long userId) {
return userService.getById(userId);
}
/**
* 根据 id 获取用户列表
* @param idList
* @return
*/
@Override
@GetMapping("/get/ids")
public List<User> listByIds(@RequestParam("idList") Collection<Long> idList) {
return userService.listByIds(idList);
}
}
- 开启 Nacos 的配置,让服务之间能够互相发现
- 所有模块引入 Nacos 依赖,然后给业务服务(包括网关)增加配置:
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
- 给业务服务项目启动类打上注解,开启服务发现、找到对应的客户端 Bean 的位置
ps:gateway 不需要添加第二个,添加第一个即可
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.yupi.yuojbackendserviceclient.service"})
- 全局引入负载均衡器依赖:(待定)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
<version>3.1.5</version>
</dependency>
- 启动项目,测试依赖能否注入,能否完成相互调用
gateway 网关
微服务网关(gateway):Gateway 聚合所有的接口,统一接受处理前端的请求,而不是多个端口分开处理。
gateway:想自定义一些功能,需要对这个技术有比较深的理解
网关:
Gateway 是应用层网关:会有一定的业务逻辑(比如根据用户信息判断权限)
Nginx 是接入层网关:比如每个请求的日志,通常没有业务逻辑
核心依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
接口路由
ps:统一地接受前端的请求,转发请求到对应的服务
通过配置:从请求路径/api//* ,根据id(spring.application.name) 来查找对应的服务
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: yuoj-backend-user-service
uri: lb://yuoj-backend-user-service
predicates:
- Path=/api/user/**
- id: yuoj-backend-question-service
uri: lb://yuoj-backend-question-service
predicates:
- Path=/api/question/**
- id: yuoj-backend-judge-service
uri: lb://yuoj-backend-judge-service
predicates:
- Path=/api/judge/**
application:
name: yuoj-backend-gateway
main:
web-application-type: reactive
server:
port: 8101
注意:spring.main.web-application-type = reactive
配置一定要加!!
聚合文档
- 先给所有业务服务引入依赖,同时开启接口文档的配置
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
knife4j:
enable: true
- 给网关服务 gateway 集中管理接口文档
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
knife4j:
gateway:
# ① 第一个配置,开启gateway聚合组件
enabled: true
# ② 第二行配置,设置聚合模式采用discover服务发现的模式
strategy: discover
discover:
# ③ 第三行配置,开启discover模式
enabled: true
# ④ 第四行配置,聚合子服务全部为Swagger2规范的文档
version: swagger2
- 访问地址即可查看聚合接口文档:http://localhost:8101/doc.html#/home
跨域问题
新建config包,添加跨域配置
// 处理跨域
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.setAllowCredentials(true);
// todo 实际改为线上真实域名、本地域名
config.setAllowedOriginPatterns(Arrays.asList("*"));
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
权限校验
新建 fileter 包
@Component
public class GlobalAuthFilter implements GlobalFilter, Ordered {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String path = serverHttpRequest.getURI().getPath();
// 判断路径中是否包含 inner,只允许内部调用
if (antPathMatcher.match("/ **/inner/** ", path)) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
DataBufferFactory dataBufferFactory = response.bufferFactory();
DataBuffer dataBuffer = dataBufferFactory.wrap("无权限".getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(dataBuffer));
}
// todo 统一权限校验,通过 JWT 获取登录用户信息
return chain.filter(exchange);
}
/**
* 优先级提到最高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}