- 第01节:如何改造为微服务架构?
1. 模块化和独立性
- 微服务:微服务架构通过将应用拆分为小型、独立的服务,每个服务专注于特定的业务功能。这种模块化的设计使得每个服务都可以独立开发、部署、扩展和维护。
- 单体服务:在单体服务中,应用是一个大而臃肿的单一单元,修改一个功能可能会影响整个应用的部署。
2. 技术异构性
- 微服务:允许使用不同的技术栈和编程语言来构建不同的服务,以适应不同的需求。每个微服务可以选择最适合其特定任务的技术。
- 单体服务:通常需要在同一技术栈下构建整个应用。
3. 独立部署和扩展
- 微服务:允许独立部署和扩展每个服务,这样可以更灵活地应对流量变化和需求变更。
- 单体服务:需要整体部署和扩展,可能会导致资源浪费或性能瓶颈。
4. 团队自治
- 微服务:每个微服务通常由一个小团队负责,团队可以根据其服务的需求进行独立的决策,提高了开发团队的自治性。
- 单体服务:整个应用的变更需要协调整个团队,可能导致开发速度较慢和沟通成本较高。
5. 弹性和容错性
- 微服务:由于每个服务都是独立的,可以更容易实现服务的弹性和容错。一个服务的故障不会影响整个应用。
- 单体服务:一个组件的故障可能导致整个应用的崩溃。
6. 可维护性和可测试性
- 微服务:每个微服务的小规模和清晰的职责范围使得代码更容易理解、维护和测试。
- 单体服务:单体应用的复杂性可能导致代码难以理解,难以维护和测试。
然后这个时候到我们的第二版本, 四个应用是分别的应用,
当然也会看到nginx来实现,这时会做一个大的虚拟体
如果班级调用的话我们我们现在短链接里面是怎么做的,知道他的IP和端口;我们当前的逻辑就是这么去实现 的;但是如果我们的人员有3个应用集群,那么localhost8080就不行的,如下图
于是我们需要一个注册中心nacos,那这个时候我们假如说我们班级服务想要调人员服务该怎么办,要知道3个应用的具体ip;因此在应用启动的时候发起服务注册;附带的消息里面必然有特征:包括服务名和ip;
班级应用中他不需要被外部所调用,那他就没必要去发起注册;但是呢他需要调用这个人员服务,那就需要去拉取;person service所对应的
要把这个我们要调用的这个应用它对应的元服务信息元数据给拿到我们本地来;这样我们在调用的时候根据我们的调用标识是他的服务名;根据服务名查询下面的ip;
如果说我重新发布了,ip是新的怎么办???实际上他会向nacos发起注销接口,把当前的实例注销掉;服务下面多见的是ip记录的;
那么remove掉后他的这个呢对应的IP他如果不变的话,我去调用他的,岂不是会失败吗;有一个优雅上下线的问题;(详见大话面试)
把这个微服务里面的注册中心说一下还有sentinel它主要就是做这个封控
短链接如何改造微服务
1. 下载 Nacos
之前 12306 本地安装的 Nacos 2.1.1 版本是否可用?可以,如果之前已经安装 Nacos,本节跳过。
Nacos GitHub 2.1.1下载地址:Release 2.1.1 (Aug 8th, 2022) · alibaba/nacos
Nacos 部署手册:Nacos支持三种部署模式 | Nacos 官网
Nacos下载、安装、使用入门级教程(windows版) - 从未被超越 - 博客园
账号密码都是nacos
2. 服务中引入 Nacos 进行服务注册
2.1. 引入 Pom 文件
服务自主发起注册。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
如果是调用方,需要引入 OpenFeign 组件
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- openfeign 已不再提供默认负载均衡器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
启动类添加 Nacos 注册中心注解
@EnableDiscoveryClient
2.2. 配置 yaml
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
改造现有代码通过 OpenFeign
调用创建 OpenFeign 远程调用服务
@FeignClient("short-link-project")
public interface ShortLinkActualRemoteService {
// 调用接口
}
业务代码中引用
private final ShortLinkActualRemoteService shortLinkActualRemoteService;
shortLinkActualRemoteService.xxx();
微服务怎么改造?首先第一下载Nacos。第二,服务中引入Nacos,进行服务注册。第三,改造现有代码通过OpenFeign调用。再然后是网关。Spring Cloud Gateway重构。前面三个比较容易理解,我们把Nacos下载到本地对吧?让它启动起来,然后我们在服务里面引入Nacos相关的包,然后把自己当前的服务注册到Nacos上面去,然后我们在调用的时候通过OpenFeign去发起调用,它的原理就是传入一个服务名,比如说我传一个person-service,然后它会通过一系列规则拿到对应的一些可用IP。当然这些有策略,默认的话我记得是轮询,我记得默认应该是轮询,还是什么我有点记不清了。我记得当时好像是Region那种形式,好像是啥我有点记不清了,之前我记得还看过他们的源码来着,然后拿到一个可用的IP之后,我们干什么?我们直接和URL一拼,拼好之后发起调用,然后拿到结果返回。然后至此的话,我们就改造成这种基于微服务架构下的多服务调用,到这里的话,我们的微服务改造已经算是完成了,为什么?因为我们生产环境都已经部署了对吧?基于是不知道阿里巴巴的生产环境的,然后我们把服务注册到Nacos里面去,然后通过OpenFeign做多服务之间的调用,然后我们会把认证用户的一些信息重构到我们的Spring Cloud Gateway里面去。
可能有同学会说,我们为什么要把相关的一些验证放到系统的外围,放到现在的网关里面,我觉得也挺好的。其实这会涉及到一个资源利用的问题,以及服务边界的事情。首先如果说纯粹的从微服务的角度上去看的话,普卡(Nacos)给它拥有更好的性能,假如说你当前的用户没有登录,你要在网关里面去验证的话,它会占用当前的连接以及当前的处理线程,你如果说验证的时候占用了其他资源,访问资源就得排队。如果说你在网关里面去做,因为它是基于Reactor的响应式模型,它是很快的,所以说我们要把一些比较通用的或者说公用的东西能抽出来的,都尽量放到网关里面去做,比如说用户认证,然后鉴权就是验签,我们都应该放到网关。然后因为我们Spring Cloud Gateway,怎么说,我个人还挺不看好它的,为什么?因为它的问题挺多的,如果说你真正生产用过的话还挺多的,但是怎么说,因为它是Spring Cloud的官方组件,系列整合里面就是开箱即用一些功能,所以说用它的人会比较多,但是这不意味着它没有问题,所以说大家以后在公司生产里面做网关调研的话,可以好好去调研一下,尽量在说尽量能引入比较新的Spring Cloud Gateway版本就引入新的,因为老版本可能会比较多好吧?你要知道引入一个版本之后,要想往后升级是有点麻烦的,因为你要倒不是说不能升,而是说你升级的话,它对应的一系列验证逻辑就会非常复杂,尤其是像网关这种门户——整个系统的前置门户,对吧?改一处所有的都得跟着,所有可能都会有影响的这种慎之又慎。所以说大家要理智一些。
- 第02节:如何将项目改造为微服务调用(上)
为admin的pom添加
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- openfeign 已不再提供默认负载均衡器 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-loadbalancer</artifactId> </dependency>
为project的pom添加
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-annotation-aspectj</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
为启动服务添加ShortLinkAdminApplication
import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients("com.nageoffer.shortlink.admin.remote") @MapperScan("com.nageoffer.shortlink.admin.dao.mapper")
配置yaml
spring: application: name: short-link-admin datasource: driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver url: jdbc:shardingsphere:classpath:shardingsphere-config.yaml data: redis: host: 192.168.111.130 password: 123321 port: 6379 cloud: nacos: discovery: server-addr: 127.0.0.1:8848
有一个h2的嵌入式内存数据库,我们已经配了mysql的数据库
接下来的改造就是把remote来改一下,新建ShortLinkActualRemoteService,删除原来的remoteservice
package com.nageoffer.shortlink.admin.remote;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nageoffer.shortlink.admin.common.convention.result.Result;
import com.nageoffer.shortlink.admin.dto.req.RecycleBinRecoverReqDTO;
import com.nageoffer.shortlink.admin.dto.req.RecycleBinRemoveReqDTO;
import com.nageoffer.shortlink.admin.dto.req.RecycleBinSaveReqDTO;
import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkBatchCreateReqDTO;
import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkCreateReqDTO;
import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkUpdateReqDTO;
import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkBatchCreateRespDTO;
import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkCreateRespDTO;
import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkGroupCountQueryRespDTO;
import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkPageRespDTO;
import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkStatsAccessRecordRespDTO;
import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkStatsRespDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
/**
* 短链接中台远程调用服务
*/
@FeignClient("short-link-project")
public interface ShortLinkActualRemoteService {
/**
* 创建短链接
*
* @param requestParam 创建短链接请求参数
* @return 短链接创建响应
*/
@PostMapping("/api/short-link/v1/create")
Result<ShortLinkCreateRespDTO> createShortLink(@RequestBody ShortLinkCreateReqDTO requestParam);
/**
* 批量创建短链接
*
* @param requestParam 批量创建短链接请求参数
* @return 短链接批量创建响应
*/
@PostMapping("/api/short-link/v1/create/batch")
Result<ShortLinkBatchCreateRespDTO> batchCreateShortLink(@RequestBody ShortLinkBatchCreateReqDTO requestParam);
/**
* 修改短链接
*
* @param requestParam 修改短链接请求参数
*/
@PostMapping("/api/short-link/v1/update")
void updateShortLink(@RequestBody ShortLinkUpdateReqDTO requestParam);
/**
* 分页查询短链接
*
* @param gid 分组标识
* @param orderTag 排序类型
* @param current 当前页
* @param size 当前数据多少
* @return 查询短链接响应
*/
@GetMapping("/api/short-link/v1/page")
Result<Page<ShortLinkPageRespDTO>> pageShortLink(@RequestParam("gid") String gid,
@RequestParam("orderTag") String orderTag,
@RequestParam("current") Long current,
@RequestParam("size") Long size);
/**
* 查询分组短链接总量
*
* @param requestParam 分组短链接总量请求参数
* @return 查询分组短链接总量响应
*/
@GetMapping("/api/short-link/v1/count")
Result<List<ShortLinkGroupCountQueryRespDTO>> listGroupShortLinkCount(@RequestParam("requestParam") List<String> requestParam);
/**
* 根据 URL 获取标题
*
* @param url 目标网站地址
* @return 网站标题
*/
@GetMapping("/api/short-link/v1/title")
Result<String> getTitleByUrl(@RequestParam("url") String url);
/**
* 保存回收站
*
* @param requestParam 请求参数
*/
@PostMapping("/api/short-link/v1/recycle-bin/save")
void saveRecycleBin(@RequestBody RecycleBinSaveReqDTO requestParam);
/**
* 分页查询回收站短链接
*
* @param gidList 分组标识集合
* @param current 当前页
* @param size 当前数据多少
* @return 查询短链接响应
*/
@GetMapping("/api/short-link/v1/recycle-bin/page")
Result<Page<ShortLinkPageRespDTO>> pageRecycleBinShortLink(@RequestParam("gidList") List<String> gidList,
@RequestParam("current") Long current,
@RequestParam("size") Long size);
/**
* 恢复短链接
*
* @param requestParam 短链接恢复请求参数
*/
@PostMapping("/api/short-link/v1/recycle-bin/recover")
void recoverRecycleBin(@RequestBody RecycleBinRecoverReqDTO requestParam);
/**
* 移除短链接
*
* @param requestParam 短链接移除请求参数
*/
@PostMapping("/api/short-link/v1/recycle-bin/remove")
void removeRecycleBin(@RequestBody RecycleBinRemoveReqDTO requestParam);
/**
* 访问单个短链接指定时间内监控数据
*
* @param fullShortUrl 完整短链接
* @param gid 分组标识
* @param startDate 开始时间
* @param endDate 结束时间
* @return 短链接监控信息
*/
@GetMapping("/api/short-link/v1/stats")
Result<ShortLinkStatsRespDTO> oneShortLinkStats(@RequestParam("fullShortUrl") String fullShortUrl,
@RequestParam("gid") String gid,
@RequestParam("startDate") String startDate,
@RequestParam("endDate") String endDate);
/**
* 访问分组短链接指定时间内监控数据
*
* @param gid 分组标识
* @param startDate 开始时间
* @param endDate 结束时间
* @return 分组短链接监控信息
*/
@GetMapping("/api/short-link/v1/stats/group")
Result<ShortLinkStatsRespDTO> groupShortLinkStats(@RequestParam("gid") String gid,
@RequestParam("startDate") String startDate,
@RequestParam("endDate") String endDate);
/**
* 访问单个短链接指定时间内监控访问记录数据
*
* @param fullShortUrl 完整短链接
* @param gid 分组标识
* @param startDate 开始时间
* @param endDate 结束时间
* @return 短链接监控访问记录信息
*/
@GetMapping("/api/short-link/v1/stats/access-record")
Result<Page<ShortLinkStatsAccessRecordRespDTO>> shortLinkStatsAccessRecord(@RequestParam("fullShortUrl") String fullShortUrl,
@RequestParam("gid") String gid,
@RequestParam("startDate") String startDate,
@RequestParam("endDate") String endDate);
/**
* 访问分组短链接指定时间内监控访问记录数据
*
* @param gid 分组标识
* @param startDate 开始时间
* @param endDate 结束时间
* @return 分组短链接监控访问记录信息
*/
@GetMapping("/api/short-link/v1/stats/access-record/group")
Result<Page<ShortLinkStatsAccessRecordRespDTO>> groupShortLinkStatsAccessRecord(@RequestParam("gid") String gid,
@RequestParam("startDate") String startDate,
@RequestParam("endDate") String endDate);
}
相应的shortLinkController
package com.nageoffer.shortlink.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nageoffer.shortlink.admin.common.convention.result.Result;
import com.nageoffer.shortlink.admin.common.convention.result.Results;
import com.nageoffer.shortlink.admin.remote.ShortLinkActualRemoteService;
import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkBatchCreateReqDTO;
import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkCreateReqDTO;
import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkPageReqDTO;
import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkUpdateReqDTO;
import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkBaseInfoRespDTO;
import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkBatchCreateRespDTO;
import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkCreateRespDTO;
import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkPageRespDTO;
import com.nageoffer.shortlink.admin.toolkit.EasyExcelWebUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 短链接后管控制层
* 公众号:马丁玩编程,回复:加群,添加马哥微信(备注:link)获取项目资料
*/
@RestController
@RequiredArgsConstructor
public class ShortLinkController {
private final ShortLinkActualRemoteService shortLinkActualRemoteService;
/**
* 创建短链接
*/
@PostMapping("/api/short-link/admin/v1/create")
public Result<ShortLinkCreateRespDTO> createShortLink(@RequestBody ShortLinkCreateReqDTO requestParam) {
return shortLinkActualRemoteService.createShortLink(requestParam);
}
/**
* 批量创建短链接
*/
@SneakyThrows
@PostMapping("/api/short-link/admin/v1/create/batch")
public void batchCreateShortLink(@RequestBody ShortLinkBatchCreateReqDTO requestParam, HttpServletResponse response) {
Result<ShortLinkBatchCreateRespDTO> shortLinkBatchCreateRespDTOResult = shortLinkActualRemoteService.batchCreateShortLink(requestParam);
if (shortLinkBatchCreateRespDTOResult.isSuccess()) {
List<ShortLinkBaseInfoRespDTO> baseLinkInfos = shortLinkBatchCreateRespDTOResult.getData().getBaseLinkInfos();
EasyExcelWebUtil.write(response, "批量创建短链接-SaaS短链接系统", ShortLinkBaseInfoRespDTO.class, baseLinkInfos);
}
}
/**
* 修改短链接
*/
@PostMapping("/api/short-link/admin/v1/update")
public Result<Void> updateShortLink(@RequestBody ShortLinkUpdateReqDTO requestParam) {
shortLinkActualRemoteService.updateShortLink(requestParam);
return Results.success();
}
/**
* 分页查询短链接
*/
@GetMapping("/api/short-link/admin/v1/page")
public Result<Page<ShortLinkPageRespDTO>> pageShortLink(ShortLinkPageReqDTO requestParam) {
return shortLinkActualRemoteService.pageShortLink(requestParam.getGid(), requestParam.getOrderTag(), requestParam.getCurrent(), requestParam.getSize());
}
}
GroupServiceImpl修改如下
public class GroupServiceImpl extends ServiceImpl<GroupMapper, GroupDO> implements GroupService { private final ShortLinkActualRemoteService shortLinkActualRemoteService; private final RedissonClient redissonClient; @Value("${short-link.group.max-num}") private Integer groupMaxNum; /* ShortLinkRemoteService shortLinkRemoteService = new ShortLinkRemoteService() { }; */ 省略省略省略省略省略省略省略省remoteservice变成ActualRemoteService省略省略省略省略省略省略省略 Result<List<ShortLinkGroupCountQueryRespDTO>> listResult = shortLinkActualRemoteService .listGroupShortLinkCount(groupDOList.stream().map(GroupDO::getGid).toList()); List<ShortLinkGroupRespDTO> shortLinkGroupRespDTOList = BeanUtil.copyToList(groupDOList, ShortLinkGroupRespDTO.class); shortLinkGroupRespDTOList.forEach(each -> { Optional<ShortLinkGroupCountQueryRespDTO> first = listResult.getData().stream() .filter(item -> Objects.equals(item.getGid(), each.getGid())) .findFirst(); first.ifPresent(item -> each.setShortLinkCount(first.get().getShortLinkCount())); });
如果调用project微服务出现业务异常,进行全局异常处理器拦截返回Result<Void>对象,这时候admin服务没有进行处理就直接赋值给Result<ShortLinkBatchCreateRespDTO>,这样不会有问题吗
马丁 回复 走完这段路:可以尝试下,应该会直接被 admin 的全局异常拦截器捕获
这里使用的Page其实就是因为openFeign是使用动态代理帮你实现了这些发送请求的功能,但是你要发送的数据要是json的,project那边只收json格式(所以得序列化)。这里通过反射来获取接口的返回对象。但是Ipage是个接口。接口是生成不了对象的,而且也不知道你对应的实现类是那个。
- 第03节:如何将项目改造为微服务调用(下)
RecycleBinController修为微服务
package com.nageoffer.shortlink.admin.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.nageoffer.shortlink.admin.common.convention.result.Result; import com.nageoffer.shortlink.admin.common.convention.result.Results; import com.nageoffer.shortlink.admin.dto.req.RecycleBinRecoverReqDTO; import com.nageoffer.shortlink.admin.dto.req.RecycleBinRemoveReqDTO; import com.nageoffer.shortlink.admin.dto.req.RecycleBinSaveReqDTO; import com.nageoffer.shortlink.admin.remote.ShortLinkActualRemoteService; import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkRecycleBinPageReqDTO; import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkPageRespDTO; import com.nageoffer.shortlink.admin.service.RecycleBinService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * 回收站管理控制层 */ @RestController @RequiredArgsConstructor public class RecycleBinController { private final ShortLinkActualRemoteService shortLinkActualRemoteService; private final RecycleBinService recycleBinService; /** * 保存回收站 */ @PostMapping("/api/short-link/admin/v1/recycle-bin/save") public Result<Void> saveRecycleBin(@RequestBody RecycleBinSaveReqDTO requestParam) { shortLinkActualRemoteService.saveRecycleBin(requestParam); return Results.success(); } /** * 分页查询回收站短链接 */ @GetMapping("/api/short-link/admin/v1/recycle-bin/page") public Result<Page<ShortLinkPageRespDTO>> pageShortLink(ShortLinkRecycleBinPageReqDTO requestParam) { return recycleBinService.pageRecycleBinShortLink(requestParam); } /** * 恢复短链接 */ @PostMapping("/api/short-link/admin/v1/recycle-bin/recover") public Result<Void> recoverRecycleBin(@RequestBody RecycleBinRecoverReqDTO requestParam) { shortLinkActualRemoteService.recoverRecycleBin(requestParam); return Results.success(); } /** * 移除短链接 */ @PostMapping("/api/short-link/admin/v1/recycle-bin/remove") public Result<Void> removeRecycleBin(@RequestBody RecycleBinRemoveReqDTO requestParam) { shortLinkActualRemoteService.removeRecycleBin(requestParam); return Results.success(); } }
同时修改RecycleBinService里面的也要用Page
RecycleBinServiceImpl修改
package com.nageoffer.shortlink.admin.service.impl; import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.nageoffer.shortlink.admin.common.biz.user.UserContext; import com.nageoffer.shortlink.admin.common.convention.exception.ServiceException; import com.nageoffer.shortlink.admin.common.convention.result.Result; import com.nageoffer.shortlink.admin.dao.entiry.GroupDO; import com.nageoffer.shortlink.admin.dao.mapper.GroupMapper; import com.nageoffer.shortlink.admin.remote.ShortLinkActualRemoteService; import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkRecycleBinPageReqDTO; import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkPageRespDTO; import com.nageoffer.shortlink.admin.service.RecycleBinService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.List; /** * URL 回收站接口实现层 */ @Service @RequiredArgsConstructor public class RecycleBinServiceImpl implements RecycleBinService { private final GroupMapper groupMapper; private final ShortLinkActualRemoteService shortLinkActualRemoteService; @Override public Result<Page<ShortLinkPageRespDTO>> pageRecycleBinShortLink(ShortLinkRecycleBinPageReqDTO requestParam) { LambdaQueryWrapper<GroupDO> queryWrapper = Wrappers.lambdaQuery(GroupDO.class) .eq(GroupDO::getUsername, UserContext.getUsername()) .eq(GroupDO::getDelFlag, 0); List<GroupDO> groupDOList = groupMapper.selectList(queryWrapper); if (CollUtil.isEmpty(groupDOList)) { throw new ServiceException("用户无分组信息"); } requestParam.setGidList(groupDOList.stream().map(GroupDO::getGid).toList()); return shortLinkActualRemoteService.pageRecycleBinShortLink(requestParam.getGidList(), requestParam.getCurrent(), requestParam.getSize()); } }
UrlTitleController怎么样?
package com.nageoffer.shortlink.admin.controller; import com.nageoffer.shortlink.admin.common.convention.result.Result; import com.nageoffer.shortlink.admin.remote.ShortLinkActualRemoteService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * URL 标题控制层 */ @RestController @RequiredArgsConstructor public class UrlTitleController { private final ShortLinkActualRemoteService shortLinkActualRemoteService; /** * 根据URL获取对应网站的标题 */ @GetMapping("/api/short-link/admin/v1/title") public Result<String> getTitleByUrl(@RequestParam("url") String url) { return shortLinkActualRemoteService.getTitleByUrl(url); } }
下啊
package com.nageoffer.shortlink.admin.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.nageoffer.shortlink.admin.common.convention.result.Result; import com.nageoffer.shortlink.admin.remote.ShortLinkActualRemoteService; import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkGroupStatsAccessRecordReqDTO; import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkGroupStatsReqDTO; import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkStatsAccessRecordReqDTO; import com.nageoffer.shortlink.admin.remote.dto.req.ShortLinkStatsReqDTO; import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkStatsAccessRecordRespDTO; import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkStatsRespDTO; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * 短链接监控控制层 */ @RestController @RequiredArgsConstructor public class ShortLinkStatsController { private final ShortLinkActualRemoteService shortLinkActualRemoteService; /** * 访问单个短链接指定时间内监控数据 */ @GetMapping("/api/short-link/admin/v1/stats") public Result<ShortLinkStatsRespDTO> shortLinkStats(ShortLinkStatsReqDTO requestParam) { return shortLinkActualRemoteService.oneShortLinkStats(requestParam.getFullShortUrl(), requestParam.getGid(), requestParam.getStartDate(), requestParam.getEndDate()); } /** * 访问分组短链接指定时间内监控数据 */ @GetMapping("/api/short-link/admin/v1/stats/group") public Result<ShortLinkStatsRespDTO> groupShortLinkStats(ShortLinkGroupStatsReqDTO requestParam) { return shortLinkActualRemoteService.groupShortLinkStats(requestParam.getGid(), requestParam.getStartDate(), requestParam.getEndDate()); } /** * 访问单个短链接指定时间内访问记录监控数据 */ @GetMapping("/api/short-link/admin/v1/stats/access-record") public Result<Page<ShortLinkStatsAccessRecordRespDTO>> shortLinkStatsAccessRecord(ShortLinkStatsAccessRecordReqDTO requestParam) { return shortLinkActualRemoteService.shortLinkStatsAccessRecord(requestParam.getFullShortUrl(), requestParam.getGid(), requestParam.getStartDate(), requestParam.getEndDate()); } /** * 访问分组短链接指定时间内访问记录监控数据 */ @GetMapping("/api/short-link/admin/v1/stats/access-record/group") public Result<Page<ShortLinkStatsAccessRecordRespDTO>> groupShortLinkStatsAccessRecord(ShortLinkGroupStatsAccessRecordReqDTO requestParam) { return shortLinkActualRemoteService.groupShortLinkStatsAccessRecord(requestParam.getGid(), requestParam.getStartDate(), requestParam.getEndDate()); } }
Is I think suddenly not. 相当于它的接近传过来。我们看一下,到家里可能是空的,在这边是房间里面。他现在时间确实界定 PPT现在是我想想,大家准备在做什么?It's ok. So teacher are people. Sure。这都不传了吗?但是具体的蜘蛛猫侠,这应该是个简单的bug,没关系,后续的话让前端那边修一下放到单个专题文件,你把它弄出来。删掉。1234-. 第二个目标orders,访问分组短链接指定时间内。应该不用我就首先是第一个,然后应该是在南平,然后删掉。
等一下。getmapper的话它都接啥?Remote. 1234相当于我们就不再传不传颜色的东西,直接把参数来取出来就可以了。第一个;第二个,我怎么看?它只能给它上面自动加码。然后你把它然后再加一个品牌,进行分组核实。我们就这三个环节你去了,但是没有完成关键阶段。搞这个分组他觉得他们的关键然后删掉。然后这个的话还成了4个参数,不要然后把这些复制上去。什么?然后检查一下报告的编一下。重启一下,然后防范监控去。可以。把防控分布出去可以也没问题,然后刚才感觉改的啥挺多的,咱们还有没有地方用的?Ok最后一个方向他就可以帮他下岗了。然后将来是我们然后我看一下监控这一块,咱们截止一定时间内我想起来了,还有这里有什么?不错了吗?不错。 Ip等一下。Ip是。可以了没问题,这次的话我们短链接中台和后管的微服务改造就到此结束了,然后我们下节课的话会跟大家讲一下,怎么把后管里面关于应用鉴权的一些事情放到我们的网关里面去,我们把代码先敲一下。
一般来说就是找到远程调用的本地接口然后复制过来,把方法体删了就是了。
远程调用里的GET请求如果有对象作为参数的话其实不需要拆开,只用加个@SpringQueryMap注解即可,例如: /** * 远程调用, 单个短链接的指定时间内各种监控 */ @GetMapping("/api/shortlink/v1/stats") Result<ShortLinkStatsRespDTO> shortLinkStats(@SpringQueryMap ShortLinkStatsReqDTO requestParam);
Mirac 回复 w:Spring Cloud OpenFeign 提供了等效的@SpringQueryMap注解,用于将 POJO 或 Map ,映射为 GET 方法的参数。
马丁 回复 w:这个挺好,我之前没用过这个,以前在公司里都是封装 OpenFeign 的扩展方法支持传递对象
- 第04节:引入网关架构SpringCloud-Gateway(上)
为什么需要网关
没有网关存在的一些问题:
- 路由管理&服务发现困难。
- 安全性难以管理:https 访问、黑白名单、用户登录和数据请求加密防篡改等。
- 负载均衡问题。
- 监控和日志难以集中管理。
- 缺乏统一的 API 管理。
前端是不知道他调用哪一个 的,如果你把这一块东西放前端了 ,违背了设计原则,就叫轻前端重后端
1. 早期应用架构
F5 最大的缺陷就是硬件贵。
2. 引入软件网关组件
3. 更复杂的网关架构
流量网关和业务网关等。
引入 SpringCloud Gateway
1. 引入 Pom 文件
引入 SpringCloud Gateway 相关的 Pom 组件。视频讲解中漏掉一个 build 标签,正常不会影响运行,但是打包的 Jar 文件不能运行,大家加上即可。
pom 文件中漏掉一个 build 标签,对代码运行没有影响,打包会有问题。所以大家补充上即可。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<!-- Unable to load io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider xxx -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<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>
因为我们网关要做这种路由转发--服务发现,就是他会把网关也注册到nacos里面去,去发现其中的短链接后管和短链接中台,
2. 创建网关启动类
package com.nageoffer.shortlink.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 网关服务应用启动器
*/
@SpringBootApplication
public class GatewayServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServiceApplication.class, args);
}
}
3. 添加网关配置文件
server:
port: 8000
spring:
application:
name: short-link-gateway
data:
redis:
host: 127.0.0.1
port: 6379
password: 123456
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: short-link-admin
uri: lb://short-link-admin/api/short-link/admin/**
predicates:
- Path=/api/short-link/admin/**
filters:
- name: TokenValidate
args:
whitePathList:
- /api/short-link/admin/v1/user/login
- /api/short-link/admin/v1/user/has-username
- id: short-link-project
uri: lb://short-link-project/api/short-link/**
predicates:
- Path=/api/short-link/**
filters:
- name: TokenValidate
4. 添加用户登录拦截器
添加白名单配置类:
package com.nageoffer.shortlink.gateway.config;
import lombok.Data;
import java.util.List;
/**
* 过滤器配置
*/
@Data
public class Config {
/**
* 白名单前置路径
*/
private List<String> whitePathList;
}
添加网关错误返回信息。
package com.nageoffer.shortlink.gateway.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 网关错误返回信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GatewayErrorResult {
/**
* HTTP 状态码
*/
private Integer status;
/**
* 返回信息
*/
private String message;
}
添加用户登录拦截器
package com.nageoffer.shortlink.gateway.filter;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.nageoffer.shortlink.gateway.config.Config;
import com.nageoffer.shortlink.gateway.dto.GatewayErrorResult;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
/**
* SpringCloud Gateway Token 拦截器
*/
@Component
public class TokenValidateGatewayFilterFactory extends AbstractGatewayFilterFactory<Config> {
private final StringRedisTemplate stringRedisTemplate;
public TokenValidateGatewayFilterFactory(StringRedisTemplate stringRedisTemplate) {
super(Config.class);
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String requestPath = request.getPath().toString();
String requestMethod = request.getMethod().name();
if (!isPathInWhiteList(requestPath, requestMethod, config.getWhitePathList())) {
String username = request.getHeaders().getFirst("username");
String token = request.getHeaders().getFirst("token");
Object userInfo;
if (StringUtils.hasText(username) && StringUtils.hasText(token) && (userInfo = stringRedisTemplate.opsForHash().get("short-link:login:" + username, token)) != null) {
JSONObject userInfoJsonObject = JSON.parseObject(userInfo.toString());
ServerHttpRequest.Builder builder = exchange.getRequest().mutate().headers(httpHeaders -> {
httpHeaders.set("userId", userInfoJsonObject.getString("id"));
httpHeaders.set("realName", URLEncoder.encode(userInfoJsonObject.getString("realName"), StandardCharsets.UTF_8));
});
return chain.filter(exchange.mutate().request(builder.build()).build());
}
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
GatewayErrorResult resultMessage = GatewayErrorResult.builder()
.status(HttpStatus.UNAUTHORIZED.value())
.message("Token validation error")
.build();
return bufferFactory.wrap(JSON.toJSONString(resultMessage).getBytes());
}));
}
return chain.filter(exchange);
};
}
private boolean isPathInWhiteList(String requestPath, String requestMethod, List<String> whitePathList) {
return (!CollectionUtils.isEmpty(whitePathList) && whitePathList.stream().anyMatch(requestPath::startsWith)) || (Objects.equals(requestPath, "/api/short-link/admin/v1/user") && Objects.equals(requestMethod, "POST"));
}
}
5. 后管系统改造事项
大概涉及到三处改动,其中还包括前端改造。
5.1. 删除用户未登录错误码
因为通过 HTTP status 401 来标识用户未登录,所以需要删除后管中的自定义错误码。
package com.nageoffer.shortlink.admin.common.enums;
import com.nageoffer.shortlink.admin.common.convention.errorcode.IErrorCode;
/**
* 用户错误码
*/
public enum UserErrorCodeEnum implements IErrorCode {
// 需要删除
USER_TOKEN_FAIL("A000200", "用户Token验证失败"),
USER_NULL("B000200", "用户记录不存在"),
USER_NAME_EXIST("B000201", "用户名已存在"),
USER_EXIST("B000202", "用户记录已存在"),
USER_SAVE_ERROR("B000203", "用户记录新增失败");
private final String code;
private final String message;
UserErrorCodeEnum(String code, String message) {
this.code = code;
this.message = message;
}
@Override
public String code() {
return code;
}
@Override
public String message() {
return message;
}
}
5.2. 修改用户拦截器
将之前的操作已经迁移至网关识别,为此,该拦截器只需要保留用户上下文代码即可。
package com.nageoffer.shortlink.admin.common.biz.user;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
/**
* 用户信息传输过滤器
*/
@RequiredArgsConstructor
public class UserTransmitFilter implements Filter {
@SneakyThrows
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String username = httpServletRequest.getHeader("username");
if (StrUtil.isNotBlank(username)) {
String userId = httpServletRequest.getHeader("userId");
String realName = httpServletRequest.getHeader("realName");
UserInfoDTO userInfoDTO = new UserInfoDTO(userId, username, realName);
UserContext.setUser(userInfoDTO);
}
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
UserContext.removeUser();
}
}
}
因为之前 Redis 操作通过构造函数创建,所以同时需要改造创建方式。
@Configuration
public class UserConfiguration {
/**
* 用户信息传递过滤器
*/
@Bean
public FilterRegistrationBean<UserTransmitFilter> globalUserTransmitFilter() {
FilterRegistrationBean<UserTransmitFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new UserTransmitFilter());
registration.addUrlPatterns("/*");
registration.setOrder(0);
return registration;
}
}
5.3. 修改前端代码
调整 vite.config.js 文件调用后端的端口需要从 8002 改为 8000 网关端口
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(/^\/api/, '') // 不可以省略rewrit
}
}
}
})
调整 axios.js 文件的用户未登录跳转方式,之前通过 res.data.code === 'A000200'
判断,现在通过 err.response.status === 401
判断。
import axios from 'axios'
import { getToken, getUsername } from '@/core/auth.js'
// import Router from '../router'
import { ElMessage } from 'element-plus'
import { isNotEmpty } from '@/utils/plugins.js'
import { useRouter } from 'vue-router'
const router = useRouter()
// const baseURL = '/resourcesharing/organizational'
const baseURL = '/api/short-link/admin/v1'
// 创建实例
const http = axios.create({
// api 代理为服务器请求地址
baseURL: '/api' + baseURL,
timeout: 15000
})
// 请求拦截 -->在请求发送之前做一些事情
http.interceptors.request.use(
(config) => {
config.headers.Token = isNotEmpty(getToken()) ? getToken() : ''
config.headers.Username = isNotEmpty(getUsername()) ? getUsername() : ''
// console.log('获取到的token和username', getToken(), getUsername())
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截 -->在返回结果之前做一些事情
http.interceptors.response.use(
(res) => {
if (res.status == 0 || res.status == 200) {
// 请求成功对响应数据做处理,此处返回的数据是axios.then(res)中接收的数据
// code值为 0 或 200 时视为成功
return Promise.resolve(res)
}
return Promise.reject(res)
},
(err) => {
// 在请求错误时要做的事儿
// 此处返回的数据是axios.catch(err)中接收的数据
if (err.response.status === 401) {
localStorage.removeItem('token')
router.push('/login')
}
return Promise.reject(err)
}
)
export default http
- 第05节:引入网关架构SpringCloud-Gateway(下)
引入网关架构后如何访问中台?
后管作为可视化界面方式操作短链接系统,中台作为提供后管接口调用以及 API 等多种调用方式。此时,中台应用就需要进行独立的用户登录验证逻辑。
密钥方式在用户记录生成时创建唯一的密钥进行保存,每次访问时都携带该密钥访问即可。
和后管沿用一套方案
和当前后管服务沿用一套登录机制,每次都带上用户的登录 Token 访问中台接口即可。Q:如果用户在后管中操作了退出登录如何解决?A:应该在客户端应用调用后,发现请求返回的 401,重新调用登录接口,再发起一次调用即可。Q:如果用户登录状态失效,会请求 401 如何解决?A:改造登录接口,如果用户已登录情况,那么重新刷新有效期。
因为我们现在的方式我更倾向于是什么?和后台去研究一套方案,为什么?因为上面这套方案对目前的现有代码改动是比较大的,然后针对于这种旁支的一些逻辑,我不是很希望去做一些主体上的改变,那这个时候就想到一种这种方法,通过我们系统的用户登录的逻辑去沿用同一份就可以了。
比如说我们现在调用通过前端去调用户管理系统它会带上我们的 token 和 talking,那这个时候我们其实只需要使用中台应用的时候,也带上我们的 token 就可以了,对吧?
这个时候会有两个问题,就是首先第一个问题如果说,用户在后台中操作了退出登录,怎么办?因为退了对不对?这个时候我们可以在客户端也就是别人的应用来调用接口系统的时候,发现返回的请求状态码是 401,这个时候他只需要去重新调用他的登录接口把值保存下来就可以了,然后再接着发起调用对吧?
这种形式有点不好是什么?每次它都有可能有一段的真空期,真空期比较短可能就几百毫秒,但是它可能会有大量的请求阻塞在这里,对吧?因为比如说它在失效的那一刻有很多的请求打过来是吧?类似于缓存击穿的这种逻辑,那就不是很优雅。
那所以说我们要有一系列的辅助方案,大家应该对接过微信公众号都知道它也是有一个在效期内对吧?通过这种 token 的机制我们也可以这样做,比如说给它进行一个续期对吧?比如说我们每次调一次 login,他就进行一个续期,微信公众号的逻辑是返回给你一个,他有这种方案,第一种的话就是你返回给你原来的,到时候你过去时间第二种强制刷新我个人觉得两种都不是很好。
我们可以采用一种这种方法就是当我们在登录的状态下就这个用户已经登录的状态下,他又掉了接口,我们可以怎么办?我们是不是可以把他的过期时间给他刷新对吧?就刷新的过期时间,这样的话不比如就比不了。如果用户登录状态失效,会触发请求指令,如何解决改造登录接口。
如果用户已登录情况,那么重新刷新有效期,这种方式就可以了,我们把代码稍微改一下,然后在这里的话我们重新给它调一下它的逻辑,就像你在这个逻辑里面给他调一下就可以。
所以如果说已经存在对吧?返回这个数据的时候,我们给它设置一个有效期,给它重新改成30分钟,这样的话就比较 ok 了,如果说想要做得更稳妥一点对吧?我们可以如果采用这种方法那种有相当于每次登录完之后,每次有人调用户接口都是给他刷新 token 的有效期,对吧?能保证它类似于永不超时的这种感觉。
Ok 这样的话我们的中台应用也能够单独的对外进行一系列的访问,优化这个方法。这样肯定是开发我们试一下好吧?
相当于假如说有别的系统想给我们导联系统,你应用里面有两个,第一个就来调用第二个接口调用其他接口调用连接的一系列接口,然后相当于你那边有个定时任务去支持,比如说每一个小时去刷一下我们的 login 的接口对吧?保证你的最新的,这样的话是比较稳妥的。
可以了,这样的话它就能保证它的一个时效时间是有在续期的情况下可能就是永不失效。Ok 那代码我写一下。
这样的话其实我们的中台相关的一系列的东西都已经比较完善了,接下来下节课我会把聚合服务给大家搭建出来,那搭建出来之后,其实大家在部署的时候只需要部署什么地位和服务就可以了,就忽略了内核和逻辑了好吧?方便大家部署的,但是真实场景下肯定还是 admin 和 project。
Mirac:好像不是我想的刷新token的功能,用户可能修改数据时突然显示token过期。
Ahci 回复 Mirac:可不可以在网关里刷新Token,username和token均不为空的情况下加一条重置过期时间就行了
马丁 回复 Ahci:可以的,实现业务的方式有很多种,只不过就看那种业务是公司实际场景需要的
不想取名 回复 Mirac:为什么用户修改数据时会突然显示token过期呢,能修改数据不是已经通过token验证了吗
TheTurtle 回复 不想取名:填完表单后点提交修改时失效了呢?
皮蛋瘦肉粥。 回复 Mirac:用户层搞个拦截器,权重最大。判断是否有token,无token就直接不拦截,有token就一直刷新redis的ttl时间。
挽倾 回复 皮蛋瘦肉粥。:hhh对的,之前做的很多项目其实都是这种思想
想问一下马哥,我觉得正常情况下分享出去的短链,点击跳转restoreUrl应该是直接网关白名单不需要鉴权的。按马哥的这个想法的话,白名单里只有hasName和login没有restoreUrl。如果我创建一个短链接,分享出去,然后我这账号退出去了,那么网关鉴权就会把restoreUrl拦下,所以马哥这里要一直定时任务续期token,保证短链创建的账号不登出,感觉有点反直觉?
一个高风亮节的人 回复 小孙要上东华:我个人理解,短连接的访问你看下路径,是直接跟在ip地址加端口后面的,根本就没加入gateway,也就是没有现在的一系列鉴权
所以可以理解为通过API调中台服务的时候还是需要开一个定时任务去不断刷后管的login接口吗 那这样的话其实通过API调用也是需要经过后管的吗
应该是说api调用中台服务还是需要经过一个网关吧,应该是的,除了短链接跳转不用
这个感觉怎么说呢感觉没什么用吧,马丁说了好多,比如说外部有人调用这个中台服务,然后说解决方案就是添加token验证,但是你也没给具体措施啊,还有就是你这边这个刷新感觉没有什么用,这不就是我不停地点击登录就一直刷新这个缓存时间吗?我哪有这么多闲工夫一直搁这点登录?你还不如在网关直接添加一个语句只要有请求过来就直接刷新用户登录缓存状态就OK了,不然的话按现在的逻辑就是每30分钟就会让用户强制退出并重新登陆,这个算是个bug吧,试问你在某个网站看电影,然后刚三十分钟就把你强制退出了,你觉得你下次还来吗?现在网上的大部分登录逻辑也差不多是这个逻辑吧
为啥这里会永不失效了呢?我的理解就是只有login时发现自己30min内登陆过才会刷新一次有效时间。但是不还要是掉login接口吗?对啊,都已经退出登录了,缓存中已经没有token了,还怎么续期。用户登录状态下,为什么还会调用登录接口?
git里面这里有一个优化用户登录接口续期逻辑: userserviceimpl里面加一句 stringRedisTemplate.expire(USER_LOGIN_KEY + requestParam.getUsername(), 30L, TimeUnit.MINUTES);
- 第06节:开发短链接聚合服务(上)
这样我们把admin和project融合为一个业务;这样的话大家最起码少100兆的一个内存对不对?为此的话参考12306的概念,我们12306的服务比较多,我是把它做成很多业务服务,都浓缩成一个叫做聚合服务了,我们在短链接里面其实也可以这么去做,为此的话我、上、将它对应的一些流程给操作了一下,然后这节课的话我就是带着大家一起跟着流程去操作,这个文档里面很有错误,但是在这个过程当中我会对它进行修复,最终大家看到的肯定是能够正常流,能够正常的执行下来的。首先我们先创建一个就是聚合服务的model(选择maven,图上忘记标记了)
test这个也可以删掉,因为我们这边是不需要用测试的。Ok聚合,首先的话它的名字给它改一下,然后我们看一下它的报名是否正确。
<?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>com.nageoffer.shortlink</groupId> <artifactId>shortlink-all</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>shortlink-aggregation</artifactId> <dependencies> <dependency> <groupId>${project.groupId}</groupId> <artifactId>shortlink-admin</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>${project.groupId}</groupId> <artifactId>shortlink-project</artifactId> <version>${project.version}</version> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <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>
在aggregation下面加一个 启动项 AggregationServiceApplication
package com.nageoffer.shortlink.aggregation; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * 短链接聚合应用 */ @EnableDiscoveryClient @SpringBootApplication(scanBasePackages = { "com.nageoffer.shortlink.admin", "com.nageoffer.shortlink.project", "com.nageoffer.shortlink.aggregation" }) @MapperScan(value = { "com.nageoffer.shortlink.project.dao.mapper", "com.nageoffer.shortlink.admin.dao.mapper" }) public class AggregationServiceApplication { public static void main(String[] args) { SpringApplication.run(AggregationServiceApplication.class, args); } }
对应的application.yaml如下
server: port: 8003 spring: application: name: short-link-aggregation datasource: driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver url: jdbc:shardingsphere:classpath:shardingsphere-config-${database.env:dev}.yaml data: redis: host: 192.168.111.130 port: 6379 password: 123321 mvc: view: prefix: /templates/ suffix: .html cloud: nacos: discovery: server-addr: 127.0.0.1:8848 aggregation: remote-url: http://127.0.0.1:${server.port} short-link: group: max-num: 20 flow-limit: enable: true time-window: 1 max-access-count: 20 domain: default: nurl.ink:8003 stats: locale: amap-key: 824c511f0997586ea016f979fdb23087 goto-domain: white-list: enable: true names: '拿个offer,知乎,掘金,博客园' details: - nageoffer.com - zhihu.com - juejin.cn - cnblogs.com mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:mapper/*.xml
还有sharingsphere
dataSources: ds_0: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root rules: - !SHARDING tables: t_user: actualDataNodes: ds_0.t_user_${0..15} tableStrategy: standard: shardingColumn: username shardingAlgorithmName: user_table_hash_mod t_group: actualDataNodes: ds_0.t_group_${0..15} tableStrategy: standard: shardingColumn: username shardingAlgorithmName: group_table_hash_mod t_link: actualDataNodes: ds_0.t_link_${0..15} tableStrategy: standard: shardingColumn: gid shardingAlgorithmName: link_table_hash_mod t_link_goto: actualDataNodes: ds_0.t_link_goto_${0..15} tableStrategy: standard: shardingColumn: full_short_url shardingAlgorithmName: link_goto_table_hash_mod t_link_stats_today: actualDataNodes: ds_0.t_link_stats_today_${0..15} tableStrategy: standard: shardingColumn: gid shardingAlgorithmName: link_stats_today_hash_mod bindingTables: - t_link, t_link_stats_today shardingAlgorithms: user_table_hash_mod: type: HASH_MOD props: sharding-count: 16 group_table_hash_mod: type: HASH_MOD props: sharding-count: 16 link_table_hash_mod: type: HASH_MOD props: sharding-count: 16 link_goto_table_hash_mod: type: HASH_MOD props: sharding-count: 16 link_stats_today_hash_mod: type: HASH_MOD props: sharding-count: 16 - !ENCRYPT tables: t_user: columns: phone: cipherColumn: phone encryptorName: common_encryptor mail: cipherColumn: mail encryptorName: common_encryptor queryWithCipherColumn: true encryptors: common_encryptor: type: AES props: aes-key-value: d6oadClrrb9A3GWo props: sql-show: true
AggregationServiceApplication.java shortlink\aggregation\src\main\java\com\nageoffer\shortlink\aggregation
application.yaml shortlink\aggregation\src\main\resources
shardingsphere-config-dev.yaml shortlink\aggregation\src\main\resources
shardingsphere-config-prod.yaml shortlink\aggregation\src\main\resources
application-aggregation.yaml shortlinkigateway(src\main\resources
application-dev.yaml shortlink\gateway\src\main\resources
application.yaml shortlink\gateway\src\main\resources
pom.xml shortlink
DataBaseConfiguration.java shortlink\project\src\main\java\com\nageoffer\shortlink\project\config
然后创建好了之后,相当于我们这个配件其实是已经包含了我的命和它的两个配置的一个组合,对吧?你比如说像这边是admin的,然后有一些是project的,它两个组合在一起就可以了,
你看这把它改掉什么意思?就是如果说聚合服务的 L等于什么时候动?幅度不对,等于空,那什么时候不等于空?当我们启动聚合服务的时候不等于空对不对?不会再从注册中心去调用他的 IP和端口了,我们直接用什么?用127.0.0.1等于自己教自己明白,相当于走路循环。正生产环境肯定会有这种代码,就相当于直连对吧?生产环境肯定不会这么大,就是为了我们自己去方便大家部署,然后加了一小块的逻辑,大家明白就好,12306也是这么做的
开始创建我们的 DEV的页面,DEV指的是分布式的模式;然后我们还有一个杠。什么意思?证明它是怎么改造的,就如果说它的生效模式是聚合的话,ok它就会有聚合的配置文件,如果说它是DEV,它就会走配置文件,正常的话我们DEV里面我们他这个老板是后面的什么对不对?然后还有shortlink对应到分布式服务下面两个不同的服务聚合,看到没有?稍微认可聚合对不对?稍微聚合,它相当于把这个东西都叫他自己就只掉一个聚合服务,相当于类似于这种。
报错了
- 第07节:开发短链接聚合服务(下)
他服务的注册方式不太对。知道了。什么时候回去?他的方式的话应该是看贝斯这个的话,我们改成改小一点就好了。如果说想要更细一点的话,我们这边可以给他搞成一个;为启动项加上那个为那些重复的项一起添加@Configuration(“...”)
然后这边那改的代码还不少呢。没事我们先改,因为你没抽组件的话确实会有这个问题。我的命。对尽快给你。两个之间好像有一样的代码。什么另外。对,不是很明显。然后我把你 by project ready stream? Just be more. User ok,没问题没关系,讲他有时候会报错,可能会比较逗比。然后这个的话革命里面我们再加一个考虑一下,然后看到了这里说的立刻看出来,这么一个代价。如果大家觉得这比较复杂的话,对吧?你如果能接受直接部门,我的命其实就不用这一步改造,如果不能接受,你就只能去做一下改造了,跟着这个视频里面肯定是能够过的。我的 user要求遥感,然后U字刻出来了。
GlobalExceptionHandler.java
@Component("globalExceptionHandlerByAdmin") @Slf4j @RestControllerAdvice
DataBaseConfiguration.java
@Configuration(value = "dataBaseConfigurationByAdmin")
MyMetaObjectHandler.java
@Primary @Component(value = "myMetaObjectHandlerByAdmin")
RBloomFilterConfiguration.java
@Configuration(value = "rBloomFilterConfigurationByAdmin")
RecycleBinController.java
@RestController(value = "shortLinkControllerByAdmin") @RequiredArgsConstructor
ShortLinkStatsController.java
@RestController(value = "shortLinkStatsControllerByAdmin") @RequiredArgsConstructor
UrITitleController.java
@RestController(value = "urlTitleControllerByAdmin") @RequiredArgsConstructor
ShortLinkActualRemoteService.java
@FeignClient(value = "short-link-project", url = "${aggregation.remote-url:}")
RecycleBinServicelmpl.java
@Service(value = "recycleBinServiceImplByAdmin")
条件注解是指在生成bean的时候会检查ioc里面是否有这么个bean
如果你不用部署,或者说部署也是分布式架构,可以跳过。聚合服务是为了帮助远程部署的同学节省内存作用,实际工作中不会存在
之前布隆过滤器里加入的短链接端口号是8001。改成聚合服务之后,配置文件里端口号需要改成8003
- 第08节:线上环境部署短链接服务(聚合服务)
首先有两个问题比较严重,第一个问题就是分页问题,分页大家可以看到我们这边是但凡是涉及到分页的,其实他都已经没有总条数了,然后他也没有那种具体的访问的一个就是分页限制,这是什么问题。首先我们知道之前我们改过一个地方,是project的config。为什么变?能够刷新。刚才因为我回滚了,这个肯定是要变的,其实它的语义是什么?@ConditionalOnBean 当存在它这里面指定了这个value,也就是比如说我指定一个这个东西@ConditionalOnBean(value= MybatisPlusInterceptor.class),它如果说supreme IOC容器里面有这个Bean它才会去加载,其实因为我们上面不是,其实如果用启动的话,其实有两个的对不对?这种情况下我们就不能再用这个onbean,其实这是我用错误的一个方法,我们正常应该用什么?你这边这个是什么意思?就是当IOC容器里面没有这bean的时候再回去加载,如果有的话就加载,这样的话是符合我们的语意的,我们把两个DataBaseConfiguration.java想改一下就好了。
@Bean @ConditionalOnMissingBean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }
首先我们就是想通过这个行为,首先一点idea这种方式启动内核,通过JDK方式不一样,的,通过idea它是通过ideal自带的被加载继续下载的,JDK本质的区别,所以说有些情况下你在idea里面是成功的,但是TOMCAT里面不一定是成功的。 我们有这个命令,因为我现在其实大家没必要去执行这个操作,我只是大家在生产给大家演示一下,其实生产上的话大家直接用GD17,我部署文档里面也是这么写的,大家直接就本地的给大家演示的话,需要去查他的环境,它的环境,就是我通过我的实际的环境直接放假去运行好吧,大家知道就好,我们重新打包一下。
访问都是404。刚才可能是因为我之前我虽然说代码回本了,但是因为我没有打包,所以说他的文件可能没有变更,然后我们刷一下,然后重新打包之后,其实就是我们当前现在的最终然后他发现全国404这是为什么?当时说实话我很困惑,就是在生产上他一直到404对吧?你报个500或者说401我都能理解,500是服务器无异常对吧?然后401是没有权限我都能理解,但是你为什么报404对不对?然后我一点点去排查,包括在网络里加日志,对吧?在我们的业务系统也就是聚合服务里面加日志等等都没有发现问题,然后最终我想了一个点,那就是看看他到底有没有进入到我们的聚合服务,发现他过网关了,但是他是围绕我们服务的,这是什么情况呢? 来找到了。因为我们组件标的大家都知道吧对不对?不是不能买我打给你,然后他其实就是将你什么部的项目变成一个可执行的大包,但是我们这边 project对吧?它其实按照我们聚合模式的部署的话,它其实这里面它是不需要加上支付法律问题了,就了解一下大家如果说,你用聚合模式,你就只能把两个文件删掉,然后不断的给删掉
那么把admin和project 的 pom.xml的repackage删除了,如果你要用分布式的部署,那么你需要重新把那两个写入
聚合模式里面AggregationService启动项是可以不要 scanBasePackages 的 "com.nageoffer.shortlink.aggregation"
fix:修复聚合服务远程部署服务器失败...(5files)
马丁那个为了推销加了关注公众号二维码的人机验证功能
他写了部署文档, 部署的时候,我也遇到这个问题了 404 ,排查了很久很久。 排查了很久,排查到请求到网关了,但是没有打到聚合服务上面。 我就像是不是 jar 包有问题啊。 果然是聚合服务 的jar 包没打好,搞了下 maven 打包插件,终于部署好了
- 第09节:如何通过域名访问线上服务
就是那样的
Comments NOTHING