本项目为开源项目
包
先了解相关包,对项目有个整体的了解。

配置相关看 官方环境搭建 就行了
站点查询相关
因为 查询大多都类似 ,比较复杂的就是首页进行车票查询,所以再这里只写一个
接口 /api/ticket-service/ticket/query ,查询车票(默认访问查询 北京到杭州的车票 )访问首页即可触发
大致流程: 请求首先会在网关中被拦截,然后通过黑白名单进行过滤,请求再到相应服务端进行处理,对于上述车票的查询,网关路由到服务端后首先查询缓存,查询不到就查询数据库,并将相关数据存储在缓存中。
具体流程如下
1 2 3 4 5 6 7 8
| @GetMapping("/api/ticket-service/ticket/query") public Result<TicketPageQueryRespDTO> pageListTicketQuery
(TicketPageQueryReqDTO requestParam) { return Results.success( ticketService.pageListTicketQueryV1(requestParam)); }
|
进入到实现层后,第一行代码就是利用 责任链模式,首先 对参数进行校验
1 2 3 4 5
| ticketPageQueryAbstractChainContext.handler
(TicketChainMarkEnum.TRAIN_QUERY_FILTER.name(), requestParam);
|
再进入到 ticketPageQueryAbstractChainContext 时,首先我们要看一个接口
1 2 3 4
| @FunctionalInterface public interface CommandLineRunner { void run(String... args) throws Exception; }
|
这是一个单接口, 文中用了大量的 @FunctionalInterface 来标识接口,我能想到的目的就是规范开发, 只定义一个接口增加灵活性,方便扩展 ?(泛型接口,不用具体实现,函数式编程直接传入具体实现)
接下来再进入 ticketPageQueryAbstractChainContext 查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| public final class AbstractChainContext<T> implements CommandLineRunner {
private final Map<String, List<AbstractChainHandler>> abstractChainHandlerContainer = new HashMap<>();
public void handler(String mark, T requestParam) { List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(mark); if (CollectionUtils.isEmpty(abstractChainHandlers)) { throw new RuntimeException(String .format("[%s] Chain of Responsibility ID is undefined.", mark)); } abstractChainHandlers. forEach(each -> each.handler(requestParam)); }
@Override public void run(String... args) throws Exception { Map<String, AbstractChainHandler> chainFilterMap = ApplicationContextHolder .getBeansOfType(AbstractChainHandler.class); chainFilterMap.forEach((beanName, bean) -> { List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(bean.mark()); if (CollectionUtils.isEmpty(abstractChainHandlers)) { abstractChainHandlers = new ArrayList(); } abstractChainHandlers.add(bean); List<AbstractChainHandler> actualAbstractChainHandlers = abstractChainHandlers.stream() .sorted(Comparator.comparing(Ordered::getOrder)) .collect(Collectors.toList()); abstractChainHandlerContainer.put(bean.mark(), actualAbstractChainHandlers); }); } }
|
DO: 上述初始化是因为实现了 CommandLineRunner 接口,这个接口只有一个方法就是 run() ,专门用于程序启动时进行初始化 ,大概逻辑如下
- 引入本模块
- 定义接口,继承
接口 AbstractChainHandler ,重写进行标注 mark,用于不同过滤器链
- 实现所定义的接口,实现方法
handler() ,处理逻辑
当该模块启动时,会自动调用 run() 方法加载到 abstractChainHandlerContainer 容器中,这样就完成了 责任链模式根据mark()标记进行不同流程验证
上述最重要的是 abstractChainHandlers.forEach(each -> each.handler(requestParam));
将参数依次传到责任链进行相关校验
之后基本上都是查询数据库写入缓存的操作,我只写逻辑,没怎么给注释
感觉代码很美,可以多看看写法,很帅
校验完成后尝试从 Redis 缓存中进行获取两地信息,如果获取不到就加锁用 双重校验锁(锁前查询一次,锁后再查询一次) 进行 数据库获取 所有站台缩写与城市之间的映射关系 查询并插入 缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| List<Object> stationDetails = stringRedisTemplate.opsForHash() .multiGet(REGION_TRAIN_STATION_MAPPING, Lists.newArrayList(requestParam.getFromStation(), requestParam.getToStation())); long count = stationDetails.stream().filter(Objects::isNull).count(); if (count > 0) { RLock lock = redissonClient.getLock(LOCK_REGION_TRAIN_STATION_MAPPING); lock.lock(); try { stationDetails = stringRedisTemplate.opsForHash() .multiGet(REGION_TRAIN_STATION_MAPPING, Lists.newArrayList(requestParam.getFromStation(), requestParam.getToStation())); count = stationDetails.stream().filter(Objects::isNull).count(); if (count > 0) { List<StationDO> stationDOList = stationMapper.selectList(Wrappers.emptyWrapper()); Map<String, String> regionTrainStationMap = new HashMap<>(); stationDOList.forEach(each -> regionTrainStationMap.put(each.getCode(), each.getRegionName())); stringRedisTemplate.opsForHash().putAll(REGION_TRAIN_STATION_MAPPING, regionTrainStationMap); stationDetails = new ArrayList<>(); stationDetails.add(regionTrainStationMap.get(requestParam.getFromStation())); stationDetails.add(regionTrainStationMap.get(requestParam.getToStation())); } } finally { lock.unlock(); } }
|
获取到数据后,再通过同样的方式,获取到 所有北京到杭州站点的车票
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| List<TicketListDTO> seatResults = new ArrayList<>(); String buildRegionTrainStationHashKey = String.format(REGION_TRAIN_STATION, stationDetails.get(0), stationDetails.get(1)); Map<Object, Object> regionTrainStationAllMap = stringRedisTemplate.opsForHash().entries(buildRegionTrainStationHashKey); if (MapUtil.isEmpty(regionTrainStationAllMap)) { RLock lock = redissonClient.getLock(LOCK_REGION_TRAIN_STATION); lock.lock(); try { regionTrainStationAllMap = stringRedisTemplate.opsForHash().entries(buildRegionTrainStationHashKey); if (MapUtil.isEmpty(regionTrainStationAllMap)) { LambdaQueryWrapper<TrainStationRelationDO> queryWrapper = Wrappers.lambdaQuery(TrainStationRelationDO.class) .eq(TrainStationRelationDO::getStartRegion, stationDetails.get(0)) .eq(TrainStationRelationDO::getEndRegion, stationDetails.get(1)); List<TrainStationRelationDO> trainStationRelationList = trainStationRelationMapper.selectList(queryWrapper); for (TrainStationRelationDO each : trainStationRelationList) { TrainDO trainDO = distributedCache.safeGet( TRAIN_INFO + each.getTrainId(), TrainDO.class, () -> trainMapper.selectById(each.getTrainId()), ADVANCE_TICKET_DAY, TimeUnit.DAYS); TicketListDTO result = new TicketListDTO(); result.setTrainId(String.valueOf(trainDO.getId())); result.setTrainNumber(trainDO.getTrainNumber()); result.setDepartureTime(convertDateToLocalTime(each.getDepartureTime(), "HH:mm")); result.setArrivalTime(convertDateToLocalTime(each.getArrivalTime(), "HH:mm")); result.setDuration(DateUtil.calculateHourDifference(each.getDepartureTime(), each.getArrivalTime())); result.setDeparture(each.getDeparture()); result.setArrival(each.getArrival()); result.setDepartureFlag(each.getDepartureFlag()); result.setArrivalFlag(each.getArrivalFlag()); result.setTrainType(trainDO.getTrainType()); result.setTrainBrand(trainDO.getTrainBrand()); if (StrUtil.isNotBlank(trainDO.getTrainTag())) { result.setTrainTags(StrUtil.split(trainDO.getTrainTag(), ",")); } long betweenDay = cn.hutool.core.date.DateUtil.betweenDay(each.getDepartureTime(), each.getArrivalTime(), false); result.setDaysArrived((int) betweenDay); result.setSaleStatus(new Date().after(trainDO.getSaleTime()) ? 0 : 1); result.setSaleTime(convertDateToLocalTime(trainDO.getSaleTime(), "MM-dd HH:mm")); seatResults.add(result); regionTrainStationAllMap.put(CacheUtil.buildKey(String.valueOf(each.getTrainId()), each.getDeparture(), each.getArrival()), JSON.toJSONString(result)); } stringRedisTemplate.opsForHash().putAll(buildRegionTrainStationHashKey, regionTrainStationAllMap); } } finally { lock.unlock(); } }
|
再获取到 车票余额、车座次相对应的价格 并进行缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| seatResults = CollUtil.isEmpty(seatResults) ? regionTrainStationAllMap.values().stream().map(each -> JSON.parseObject(each.toString(), TicketListDTO.class)).toList() : seatResults; seatResults = seatResults.stream().sorted(new TimeStringComparator()).toList(); for (TicketListDTO each : seatResults) { String trainStationPriceStr = distributedCache.safeGet( String.format(TRAIN_STATION_PRICE, each.getTrainId(), each.getDeparture(), each.getArrival()), String.class, () -> { LambdaQueryWrapper<TrainStationPriceDO> trainStationPriceQueryWrapper = Wrappers.lambdaQuery(TrainStationPriceDO.class) .eq(TrainStationPriceDO::getDeparture, each.getDeparture()) .eq(TrainStationPriceDO::getArrival, each.getArrival()) .eq(TrainStationPriceDO::getTrainId, each.getTrainId()); return JSON.toJSONString(trainStationPriceMapper.selectList(trainStationPriceQueryWrapper)); }, ADVANCE_TICKET_DAY, TimeUnit.DAYS ); List<TrainStationPriceDO> trainStationPriceDOList = JSON.parseArray(trainStationPriceStr, TrainStationPriceDO.class); List<SeatClassDTO> seatClassList = new ArrayList<>(); trainStationPriceDOList.forEach(item -> { String seatType = String.valueOf(item.getSeatType()); String keySuffix = StrUtil.join("_", each.getTrainId(), item.getDeparture(), item.getArrival()); Object quantityObj = stringRedisTemplate.opsForHash().get(TRAIN_STATION_REMAINING_TICKET + keySuffix, seatType); int quantity = Optional.ofNullable(quantityObj) .map(Object::toString) .map(Integer::parseInt) .orElseGet(() -> { Map<String, String> seatMarginMap = seatMarginCacheLoader.load(String.valueOf(each.getTrainId()), seatType, item.getDeparture(), item.getArrival()); return Optional.ofNullable(seatMarginMap.get(String.valueOf(item.getSeatType()))).map(Integer::parseInt).orElse(0); }); seatClassList.add(new SeatClassDTO(item.getSeatType(), quantity, new BigDecimal(item.getPrice()).divide(new BigDecimal("100"), 1, RoundingMode.HALF_UP), false)); }); each.setSeatClassList(seatClassList); }
|
到这里,基本上就结束了,中间三个代码段都是 对车票部分进行查询、缓存,还有一部分不好的地方,我找个时间补上。
总结一下
这个项目查询车票流程基本这样,对于 网关 实现方式多,本项目是通过继承 AbstractGatewayFilterFactory ,主要重写 apply 方法实现。
有很多代码写得很美,
像一开始的责任链模式进行参数校验,责任链实现在包frameworks.designpattern(前几太看的时候我还记得是如何加载的,现在我已经忘了T_T)
然后就是用 @FunctionalInterface只写一个接口,文中用了很多,举个例子
1 2 3 4 5 6 7 8 9 10
| @Override public List<StationQueryRespDTO> listAllStation() { return distributedCache.safeGet( STATION_ALL, List.class, () -> BeanUtil.convert(stationMapper.selectList(Wrappers.emptyWrapper()), StationQueryRespDTO.class), ADVANCE_TICKET_DAY, TimeUnit.DAYS ); }
|
其中 () -> BeanUtil.convert(stationMapper.selectList(Wrappers.emptyWrapper()), StationQueryRespDTO.class) 是实现 CacheLoader 的 load() 接口
1 2 3 4 5 6 7 8
| @FunctionalInterface public interface CacheLoader<T> {
T load(); }
|
这有什么好处呢?
我们写这样一个接口,通过函数式编程方式,直接写上实现。不用再依次写具体实现,然后再调用,极大增强复用性,查这个写一个实现,查那个写一个实现
雅,太雅了!