Commit d91374865fb33ea96a5ec6c30c9a9b2d04d5d29f

Authored by shuhongfan
1 parent 97b0ec46

day11-物流信息微服务

sl-express-ms-transport-info-service/pom.xml
... ... @@ -49,6 +49,10 @@
49 49 <artifactId>spring-boot-starter-web</artifactId>
50 50 </dependency>
51 51 <dependency>
  52 + <groupId>org.redisson</groupId>
  53 + <artifactId>redisson</artifactId>
  54 + </dependency>
  55 + <dependency>
52 56 <groupId>com.alibaba.cloud</groupId>
53 57 <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
54 58 </dependency>
... ...
sl-express-ms-transport-info-service/src/main/java/com/sl/transport/info/config/BloomFilterConfig.java 0 → 100644
  1 +package com.sl.transport.info.config;
  2 +
  3 +import lombok.Getter;
  4 +import org.springframework.beans.factory.annotation.Value;
  5 +import org.springframework.context.annotation.Configuration;
  6 +
  7 +/**
  8 + * 布隆过滤器相关配置
  9 + */
  10 +@Getter
  11 +@Configuration
  12 +public class BloomFilterConfig {
  13 +
  14 + /**
  15 + * 名称,默认:sl-bloom-filter
  16 + */
  17 + @Value("${bloom.name:sl-bloom-filter}")
  18 + private String name;
  19 +
  20 + /**
  21 + * 布隆过滤器长度,最大支持Integer.MAX_VALUE*2,即:4294967294,默认:1千万
  22 + */
  23 + @Value("${bloom.expectedInsertions:10000000}")
  24 + private long expectedInsertions;
  25 +
  26 + /**
  27 + * 误判率,默认:0.05
  28 + */
  29 + @Value("${bloom.falseProbability:0.05d}")
  30 + private double falseProbability;
  31 +
  32 +}
... ...
sl-express-ms-transport-info-service/src/main/java/com/sl/transport/info/config/CaffeineConfig.java 0 → 100644
  1 +package com.sl.transport.info.config;
  2 +
  3 +import com.github.benmanes.caffeine.cache.Cache;
  4 +import com.github.benmanes.caffeine.cache.Caffeine;
  5 +import com.sl.transport.info.domain.TransportInfoDTO;
  6 +import org.springframework.beans.factory.annotation.Value;
  7 +import org.springframework.context.annotation.Bean;
  8 +import org.springframework.context.annotation.Configuration;
  9 +
  10 +
  11 +/**
  12 + * Caffeine缓存配置
  13 + */
  14 +@Configuration
  15 +public class CaffeineConfig {
  16 +
  17 + @Value("${caffeine.init}")
  18 + private Integer init;
  19 +
  20 + @Value("${caffeine.max}")
  21 + private Integer max;
  22 +
  23 + @Bean
  24 + public Cache<String, TransportInfoDTO> transportInfoCache() {
  25 + return Caffeine.newBuilder()
  26 + .initialCapacity(init)
  27 + .maximumSize(max)
  28 + .build();
  29 + }
  30 +
  31 +}
... ...
sl-express-ms-transport-info-service/src/main/java/com/sl/transport/info/config/MyRedisCacheManager.java 0 → 100644
  1 +package com.sl.transport.info.config;
  2 +
  3 +import cn.hutool.core.util.ObjectUtil;
  4 +import cn.hutool.core.util.RandomUtil;
  5 +import org.springframework.data.redis.cache.RedisCache;
  6 +import org.springframework.data.redis.cache.RedisCacheConfiguration;
  7 +import org.springframework.data.redis.cache.RedisCacheManager;
  8 +import org.springframework.data.redis.cache.RedisCacheWriter;
  9 +
  10 +import java.time.Duration;
  11 +
  12 +/**
  13 + * 自定义CacheManager,用于设置不同的过期时间,防止雪崩问题的发生
  14 + */
  15 +public class MyRedisCacheManager extends RedisCacheManager {
  16 +
  17 + public MyRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
  18 + super(cacheWriter, defaultCacheConfiguration);
  19 + }
  20 +
  21 + @Override
  22 + protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
  23 + //获取到原有过期时间
  24 + Duration duration = cacheConfig.getTtl();
  25 + if (ObjectUtil.isNotEmpty(duration)) {
  26 + //在原有时间上随机增加1~10分钟
  27 + Duration newDuration = duration.plusMinutes(RandomUtil.randomInt(1, 11));
  28 + cacheConfig = cacheConfig.entryTtl(newDuration);
  29 + }
  30 + return super.createRedisCache(name, cacheConfig);
  31 + }
  32 +}
... ...
sl-express-ms-transport-info-service/src/main/java/com/sl/transport/info/config/RedisConfig.java 0 → 100644
  1 +package com.sl.transport.info.config;
  2 +
  3 +import io.lettuce.core.dynamic.RedisCommandFactory;
  4 +import org.springframework.beans.factory.annotation.Value;
  5 +import org.springframework.context.annotation.Bean;
  6 +import org.springframework.context.annotation.Configuration;
  7 +import org.springframework.data.redis.cache.RedisCacheConfiguration;
  8 +import org.springframework.data.redis.cache.RedisCacheManager;
  9 +import org.springframework.data.redis.cache.RedisCacheWriter;
  10 +import org.springframework.data.redis.connection.RedisConnectionFactory;
  11 +import org.springframework.data.redis.core.RedisTemplate;
  12 +import org.springframework.data.redis.listener.ChannelTopic;
  13 +import org.springframework.data.redis.listener.RedisMessageListenerContainer;
  14 +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
  15 +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
  16 +import org.springframework.data.redis.serializer.RedisSerializationContext;
  17 +import org.springframework.data.redis.serializer.StringRedisSerializer;
  18 +
  19 +import java.time.Duration;
  20 +
  21 +@Configuration
  22 +public class RedisConfig {
  23 +
  24 + public static final String CHANNEL_TOPIC = "sl-express-ms-transport-info-caffeine";
  25 +
  26 + /**
  27 + * 存储的默认有效期时间,单位:小时
  28 + */
  29 + @Value("${redis.ttl:1}")
  30 + private Integer redisTtl;
  31 +
  32 + @Bean
  33 + public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
  34 + // 默认配置
  35 + RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
  36 + // 设置key的序列化方式为字符串
  37 + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
  38 + // 设置value的序列化方式为json格式
  39 + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
  40 + .disableCachingNullValues() // 不缓存null
  41 + .entryTtl(Duration.ofHours(redisTtl)); // 默认缓存数据保存1小时
  42 +
  43 + // 构redis缓存管理器
  44 +// RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder
  45 +// .fromConnectionFactory(redisTemplate.getConnectionFactory())
  46 +// .cacheDefaults(defaultCacheConfiguration)
  47 +// .transactionAware() // 只在事务成功提交后才会进行缓存的put/evict操作
  48 +// .build();
  49 +
  50 + RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
  51 + MyRedisCacheManager myRedisCacheManager = new MyRedisCacheManager(redisCacheWriter, defaultCacheConfiguration);
  52 + myRedisCacheManager.setTransactionAware(true);
  53 + return myRedisCacheManager;
  54 + }
  55 +
  56 + /**
  57 + * 配置订阅,用于解决Caffeine一致性的问题
  58 + *
  59 + * @param redisCommandFactory 链接工厂
  60 + * @param messageListenerAdapter 消息监听器
  61 + * @return 消息监听容器
  62 + */
  63 + @Bean
  64 + public RedisMessageListenerContainer container(
  65 + RedisConnectionFactory redisCommandFactory,
  66 + MessageListenerAdapter messageListenerAdapter
  67 + ) {
  68 + RedisMessageListenerContainer container = new RedisMessageListenerContainer();
  69 + container.setConnectionFactory(redisCommandFactory);
  70 + container.addMessageListener(messageListenerAdapter, new ChannelTopic(CHANNEL_TOPIC));
  71 + return container;
  72 + }
  73 +
  74 +
  75 +}
... ...
sl-express-ms-transport-info-service/src/main/java/com/sl/transport/info/config/RedissonConfiguration.java 0 → 100644
  1 +package com.sl.transport.info.config;
  2 +
  3 +
  4 +import cn.hutool.core.convert.Convert;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import org.redisson.Redisson;
  7 +import org.redisson.api.RedissonClient;
  8 +import org.redisson.config.Config;
  9 +import org.redisson.config.SingleServerConfig;
  10 +import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
  11 +import org.springframework.context.annotation.Bean;
  12 +import org.springframework.context.annotation.Configuration;
  13 +
  14 +import javax.annotation.Resource;
  15 +
  16 +@Configuration
  17 +public class RedissonConfiguration {
  18 +
  19 + @Resource
  20 + private RedisProperties redisProperties;
  21 +
  22 + @Bean
  23 + public RedissonClient redissonSingle() {
  24 + Config config = new Config();
  25 + SingleServerConfig serverConfig = config.useSingleServer()
  26 + .setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort());
  27 + if (null != (redisProperties.getTimeout())) {
  28 + serverConfig.setTimeout(1000 * Convert.toInt(redisProperties.getTimeout().getSeconds()));
  29 + }
  30 + if (StrUtil.isNotEmpty(redisProperties.getPassword())) {
  31 + serverConfig.setPassword(redisProperties.getPassword());
  32 + }
  33 + return Redisson.create(config);
  34 + }
  35 +
  36 +}
... ...
sl-express-ms-transport-info-service/src/main/java/com/sl/transport/info/controller/TransportInfoController.java
1 1 package com.sl.transport.info.controller;
2 2  
  3 +import com.github.benmanes.caffeine.cache.Cache;
3 4 import com.sl.transport.common.exception.SLException;
4 5 import com.sl.transport.common.util.BeanUtil;
5 6 import com.sl.transport.common.util.ObjectUtil;
  7 +import com.sl.transport.info.config.CaffeineConfig;
6 8 import com.sl.transport.info.domain.TransportInfoDTO;
7 9 import com.sl.transport.info.entity.TransportInfoEntity;
8 10 import com.sl.transport.info.enums.ExceptionEnum;
  11 +import com.sl.transport.info.service.BloomFilterService;
9 12 import com.sl.transport.info.service.TransportInfoService;
10 13 import io.swagger.annotations.Api;
11 14 import io.swagger.annotations.ApiImplicitParam;
12 15 import io.swagger.annotations.ApiImplicitParams;
13 16 import io.swagger.annotations.ApiOperation;
  17 +import org.springframework.beans.factory.annotation.Autowired;
14 18 import org.springframework.web.bind.annotation.GetMapping;
15 19 import org.springframework.web.bind.annotation.PathVariable;
16 20 import org.springframework.web.bind.annotation.RequestMapping;
... ... @@ -26,6 +30,12 @@ public class TransportInfoController {
26 30 @Resource
27 31 private TransportInfoService transportInfoService;
28 32  
  33 + @Resource
  34 + public Cache<String, TransportInfoDTO> transportInfoCache;
  35 +
  36 + @Autowired
  37 + private BloomFilterService bloomFilterService;
  38 +
29 39 /**
30 40 * 根据运单id查询运单信息
31 41 *
... ... @@ -38,10 +48,23 @@ public class TransportInfoController {
38 48 @ApiOperation(value = "查询", notes = "根据运单id查询物流信息")
39 49 @GetMapping("{transportOrderId}")
40 50 public TransportInfoDTO queryByTransportOrderId(@PathVariable("transportOrderId") String transportOrderId) {
41   - TransportInfoEntity transportInfoEntity = transportInfoService.queryByTransportOrderId(transportOrderId);
42   - if (ObjectUtil.isNotEmpty(transportInfoEntity)) {
  51 +// 如果布隆过滤器中不存在,无需缓存命中,直接返回即可
  52 + boolean contains = bloomFilterService.contains(transportOrderId);
  53 + if (!contains) {
  54 + throw new SLException(ExceptionEnum.NOT_FOUND);
  55 + }
  56 +
  57 + TransportInfoDTO transportInfoDTO = transportInfoCache.get(transportOrderId, s -> {
  58 +// 未命中,查询MongoMD
  59 + TransportInfoEntity transportInfoEntity = this.transportInfoService.queryByTransportOrderId(transportOrderId);
  60 +// 转换成DTO
43 61 return BeanUtil.toBean(transportInfoEntity, TransportInfoDTO.class);
  62 + });
  63 +
  64 + if (ObjectUtil.isNotEmpty(transportInfoDTO)) {
  65 + return transportInfoDTO;
44 66 }
  67 +
45 68 throw new SLException(ExceptionEnum.NOT_FOUND);
46 69 }
47 70  
... ...
sl-express-ms-transport-info-service/src/main/java/com/sl/transport/info/mq/RedisMessageListener.java 0 → 100644
  1 +package com.sl.transport.info.mq;
  2 +
  3 +import cn.hutool.core.convert.Convert;
  4 +import com.github.benmanes.caffeine.cache.Cache;
  5 +import com.sl.transport.info.domain.TransportInfoDTO;
  6 +import org.springframework.beans.factory.annotation.Autowired;
  7 +import org.springframework.data.redis.connection.Message;
  8 +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
  9 +import org.springframework.stereotype.Component;
  10 +
  11 +import javax.annotation.Resource;
  12 +
  13 +/**
  14 + * redis消息监听,解决Caffeine一致性的问题
  15 + */
  16 +@Component
  17 +public class RedisMessageListener extends MessageListenerAdapter {
  18 +
  19 + @Resource
  20 + public Cache<String, TransportInfoDTO> transportInfoCache;
  21 +
  22 + @Override
  23 + public void onMessage(Message message, byte[] pattern) {
  24 +// 获取到消息中的运单ID
  25 + String transportOrderId = Convert.toStr(message);
  26 +
  27 +// 将本JVM中的缓存删除掉
  28 + transportInfoCache.invalidate(transportOrderId);
  29 + }
  30 +}
... ...
sl-express-ms-transport-info-service/src/main/java/com/sl/transport/info/mq/TransportInfoMQListener.java
1 1 package com.sl.transport.info.mq;
2 2  
  3 +import cn.hutool.core.convert.Convert;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.hutool.json.JSONUtil;
  6 +import com.sl.ms.transport.api.OrganFeign;
3 7 import com.sl.transport.common.constant.Constants;
  8 +import com.sl.transport.common.vo.TransportInfoMsg;
  9 +import com.sl.transport.domain.OrganDTO;
  10 +import com.sl.transport.info.entity.TransportInfoDetail;
  11 +import com.sl.transport.info.service.TransportInfoService;
4 12 import org.springframework.amqp.core.ExchangeTypes;
5 13 import org.springframework.amqp.rabbit.annotation.Exchange;
6 14 import org.springframework.amqp.rabbit.annotation.Queue;
7 15 import org.springframework.amqp.rabbit.annotation.QueueBinding;
8 16 import org.springframework.amqp.rabbit.annotation.RabbitListener;
  17 +import org.springframework.beans.factory.annotation.Autowired;
9 18 import org.springframework.stereotype.Component;
10 19 import org.springframework.transaction.annotation.Transactional;
11 20  
... ... @@ -15,6 +24,13 @@ import org.springframework.transaction.annotation.Transactional;
15 24 @Component
16 25 public class TransportInfoMQListener {
17 26  
  27 + @Autowired
  28 + private OrganFeign organFeign;
  29 +
  30 + @Autowired
  31 + private TransportInfoService transportInfoService;
  32 +
  33 +
18 34 @RabbitListener(bindings = @QueueBinding(
19 35 value = @Queue(name = Constants.MQ.Queues.TRANSPORT_INFO_APPEND),
20 36 exchange = @Exchange(name = Constants.MQ.Exchanges.TRANSPORT_INFO, type = ExchangeTypes.TOPIC),
... ... @@ -23,6 +39,29 @@ public class TransportInfoMQListener {
23 39 @Transactional
24 40 public void listenTransportInfoMsg(String msg) {
25 41 //{"info":"您的快件已到达【$organId】", "status":"运输中", "organId":90001, "transportOrderId":"SL920733749248" , "created":1653133234913}
  42 + TransportInfoMsg transportInfoMsg = JSONUtil.toBean(msg, TransportInfoMsg.class);
  43 + Long organId = transportInfoMsg.getOrganId();
  44 + String transportOrderId = Convert.toStr(transportInfoMsg.getTransportOrderId());
  45 + String info = transportInfoMsg.getInfo();
  46 +
  47 +// 查询机构信息
  48 + if (StrUtil.contains(info, "$organId")) {
  49 + OrganDTO organDTO = organFeign.queryById(organId);
  50 + if (organDTO == null) {
  51 + return;
  52 + }
  53 + info = StrUtil.replace(info, "$organId", organDTO.getName());
  54 + }
  55 +
  56 +// 封装Detail对象
  57 + TransportInfoDetail infoDetail = TransportInfoDetail.builder()
  58 + .info(info)
  59 + .status(transportInfoMsg.getStatus())
  60 + .created(transportInfoMsg.getCreated())
  61 + .build();
  62 +
  63 +// 存储到MongoDB
  64 + transportInfoService.saveOrUpdate(transportOrderId, infoDetail);
26 65  
27 66 }
28 67 }
... ...
sl-express-ms-transport-info-service/src/main/java/com/sl/transport/info/service/BloomFilterService.java 0 → 100644
  1 +package com.sl.transport.info.service;
  2 +
  3 +/**
  4 + * 布隆过滤器服务
  5 + */
  6 +public interface BloomFilterService {
  7 +
  8 + /**
  9 + * 初始化布隆过滤器
  10 + */
  11 + void init();
  12 +
  13 + /**
  14 + * 向布隆过滤器中添加数据
  15 + *
  16 + * @param obj 待添加的数据
  17 + * @return 是否成功
  18 + */
  19 + boolean add(Object obj);
  20 +
  21 + /**
  22 + * 判断数据是否存在
  23 + *
  24 + * @param obj 数据
  25 + * @return 是否存在
  26 + */
  27 + boolean contains(Object obj);
  28 +
  29 +}
... ...
sl-express-ms-transport-info-service/src/main/java/com/sl/transport/info/service/impl/BloomFilterServiceImpl.java 0 → 100644
  1 +package com.sl.transport.info.service.impl;
  2 +
  3 +import com.sl.transport.info.config.BloomFilterConfig;
  4 +import com.sl.transport.info.service.BloomFilterService;
  5 +import org.redisson.api.RBloomFilter;
  6 +import org.redisson.api.RedissonClient;
  7 +import org.springframework.beans.factory.annotation.Autowired;
  8 +import org.springframework.stereotype.Service;
  9 +
  10 +import javax.annotation.PostConstruct;
  11 +
  12 +@Service
  13 +public class BloomFilterServiceImpl implements BloomFilterService {
  14 +
  15 + @Autowired
  16 + private RedissonClient redissonClient;
  17 +
  18 + @Autowired
  19 + private BloomFilterConfig bloomFilterConfig;
  20 +
  21 + private RBloomFilter<Object> getBloomFilter() {
  22 + return redissonClient.getBloomFilter(this.bloomFilterConfig.getName());
  23 + }
  24 +
  25 + @Override
  26 + @PostConstruct
  27 + public void init() {
  28 + RBloomFilter<Object> bloomFilter = getBloomFilter();
  29 + bloomFilter.tryInit(
  30 + bloomFilterConfig.getExpectedInsertions(),
  31 + bloomFilterConfig.getFalseProbability());
  32 + }
  33 +
  34 + @Override
  35 + public boolean add(Object obj) {
  36 + return getBloomFilter().add(obj);
  37 + }
  38 +
  39 + @Override
  40 + public boolean contains(Object obj) {
  41 + return getBloomFilter().contains(obj);
  42 + }
  43 +
  44 +}
... ...
sl-express-ms-transport-info-service/src/main/java/com/sl/transport/info/service/impl/TransportInfoServiceImpl.java 0 → 100644
  1 +package com.sl.transport.info.service.impl;
  2 +
  3 +import cn.hutool.core.collection.ListUtil;
  4 +import com.github.benmanes.caffeine.cache.Cache;
  5 +import com.sl.transport.common.util.ObjectUtil;
  6 +import com.sl.transport.info.config.RedisConfig;
  7 +import com.sl.transport.info.entity.TransportInfoDetail;
  8 +import com.sl.transport.info.entity.TransportInfoEntity;
  9 +import com.sl.transport.info.service.TransportInfoService;
  10 +import org.springframework.beans.factory.annotation.Autowired;
  11 +import org.springframework.cache.annotation.CachePut;
  12 +import org.springframework.cache.annotation.Cacheable;
  13 +import org.springframework.data.mongodb.core.MongoTemplate;
  14 +import org.springframework.data.mongodb.core.query.Criteria;
  15 +import org.springframework.data.mongodb.core.query.Query;
  16 +import org.springframework.data.redis.core.StringRedisTemplate;
  17 +import org.springframework.stereotype.Service;
  18 +
  19 +@Service
  20 +public class TransportInfoServiceImpl implements TransportInfoService {
  21 +
  22 + @Autowired
  23 + private MongoTemplate mongoTemplate;
  24 + @Autowired
  25 + private Cache transportInfoCache;
  26 + private StringRedisTemplate stringRedisTemplate;
  27 + @Autowired
  28 + private BloomFilterServiceImpl bloomFilterService;
  29 +
  30 + /**
  31 + * 如果运单数据不存在,就新增,否则更新数据
  32 + *
  33 + * @param transportOrderId 运单id
  34 + * @param infoDetail 信息详情
  35 + * @return 运输信息数据
  36 + */
  37 + @Override
  38 + @CachePut(value = "transport-info",key = "#p0")
  39 + public TransportInfoEntity saveOrUpdate(String transportOrderId, TransportInfoDetail infoDetail) {
  40 +// 根据运单ID查询
  41 + Query query = Query.query(Criteria.where("transportOrderId").is(transportOrderId));
  42 + TransportInfoEntity transportInfoEntity = mongoTemplate.findOne(query, TransportInfoEntity.class);
  43 + if (ObjectUtil.isEmpty(transportInfoEntity)) {
  44 +// 运单信息不存在,新增数据
  45 + transportInfoEntity = new TransportInfoEntity();
  46 + transportInfoEntity.setTransportOrderId(transportOrderId);
  47 + transportInfoEntity.setInfoList(ListUtil.toList(infoDetail));
  48 + transportInfoEntity.setCreated(System.currentTimeMillis());
  49 +
  50 +// 写到布隆过滤器中
  51 + bloomFilterService.add(transportOrderId);
  52 + } else {
  53 +// 运单信息存在,只需要追加物流详情数据
  54 + transportInfoEntity.getInfoList().add(infoDetail);
  55 + }
  56 +
  57 +// 无论新增还是更新都要设置更新时间
  58 + transportInfoEntity.setUpdated(System.currentTimeMillis());
  59 +
  60 +// 清除缓存中的数据
  61 +// transportInfoCache.invalidate(transportOrderId);
  62 + stringRedisTemplate.convertAndSend(RedisConfig.CHANNEL_TOPIC, transportOrderId);
  63 +
  64 + return mongoTemplate.save(transportInfoEntity);
  65 + }
  66 +
  67 + /**
  68 + * 根据运单id查询运输信息
  69 + *
  70 + * @param transportOrderId 运单id
  71 + * @return 运输信息数据
  72 + */
  73 + @Override
  74 + @Cacheable(value = "transport-info", key = "#p0")
  75 + public TransportInfoEntity queryByTransportOrderId(String transportOrderId) {
  76 + Query query = Query.query(Criteria.where("transportOrderId").is(transportOrderId));
  77 + return mongoTemplate.findOne(query, TransportInfoEntity.class);
  78 + }
  79 +
  80 +}
... ...