项目实战知识点-抽奖系统

1. 有关JSON的序列化和反序列化

1.1 常规写法

java 复制代码
@Test
    void jacksonTest(){
        ObjectMapper objectMapper=new ObjectMapper();

        CommonResult<String> result=CommonResult.error(500,"系统错误");
        String str;

        // 序列化  对象->json
        try {
            str=objectMapper.writeValueAsString(result);
            System.out.println(str);
        }catch (JacksonException e){
            throw new RuntimeException(e);
        }

        // 反序列化
        try {
            CommonResult<String> readResult =objectMapper.readValue(str,CommonResult.class);
            System.out.println(readResult.getCode()+" "+readResult.getMsg());
        }catch (JacksonException e){
            throw new RuntimeException(e);
        }

        // List 序列化
        List<CommonResult<String>> commonResults= Arrays.asList(
                CommonResult.success("success1"),
                CommonResult.success("success2")
        );
        try {
            str =objectMapper.writeValueAsString(commonResults);
            System.out.println(str);
        }catch (JacksonException e){
            throw new RuntimeException(e);
        }

        // 反序列化
        JavaType javaType=objectMapper.getTypeFactory()
                .constructParametricType(List.class,CommonResult.class);
        try {
            commonResults=objectMapper.readValue(str,javaType);
            for (CommonResult<String> commonResult:commonResults){
                System.out.println(commonResult.getData());
            }
        }catch (JacksonException e){
            throw new RuntimeException(e);
        }
    }

缺陷:一直要写 try-catch,如果封装为方法接口,调用接口的时候也需要去抛异常

1.2 借鉴方法

在 package org.springframework.boot.json; 下的 JacksonJsonParser 类中有写序列化和反序列化的方法,反序列都只调用了一个方法:tryParse

java 复制代码
@Override
public Map<String, Object> parseMap(@Nullable String json) {
	return tryParse(() -> getJsonMapper().readValue(json, MAP_TYPE), Exception.class);
}

@Override
public List<Object> parseList(@Nullable String json) {
	return tryParse(() -> getJsonMapper().readValue(json, LIST_TYPE), Exception.class);
}

进入 tryParse 方法

java 复制代码
protected final <T> T tryParse(Callable<T> parser, Class<? extends Exception> check) {
		try {
			return parser.call();
		}
		catch (Exception ex) {
            // 判断是否为预料中的异常
			if (check.isAssignableFrom(ex.getClass())) {
				throw new JsonParseException(ex);
			}
			ReflectionUtils.rethrowRuntimeException(ex);
			throw new IllegalStateException(ex);
		}
	}

return parser.call(); 调用 Lambda 表达式(有抛出异常)

将所有的异常在 tryParse 中进行了catch

tryParse 方法在内部捕获了所有异常(包括受检异常),然后根据条件:

  • 如果是"预期"的异常(check.isAssignableFrom(ex.getClass())),包装成 JsonParseException(假设它继承 RuntimeException

  • 如果不是,则用 ReflectionUtils.rethrowRuntimeException(ex) 或抛出 IllegalStateException(都是非受检异常)

不需要 try-catch,也不需要 throws 。因为 tryParse 方法没有声明任何受检异常(它只抛出运行时异常),编译器不会强制你捕获

1.3 实现代码

1.4 测试代码

2. SLF4J+logback的配置

2.1 新增 application.properties 配置

java 复制代码
## logback xml ##
logging.config=classpath:logback-spring.xml
spring.profiles.active=dev
# 部署后需要变成
# spring.profiles.active=test

2.2 新增 logback-spring.xml

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration  scan="true" scanPeriod="60 seconds" debug="false">
<springProfile name="dev"............

2.3 自定义过滤器

java 复制代码
public class InfoLevelFilter {   // 定义一个名为 InfoLevelFilter 的过滤器类
    public FilterReply decide(ILoggingEvent iLoggingEvent) {  
        // decide 方法是 Logback 过滤器的核心,每次产生日志事件时都会调用
        // 参数 iLoggingEvent 代表一条待输出的日志事件(包含级别、内容、时间等)

        if(iLoggingEvent.getLevel().toInt() == Level.INFO_INT) {
            // 如果这条日志的级别转换成整数后,等于 INFO 级别的整数值
            return FilterReply.ACCEPT;   // 接受:这条日志会被输出
        }
        return FilterReply.DENY;         // 否则拒绝:这条日志不会被输出
    }
}

Logback 就是 Java 应用中最常用的高性能日志框架,它实现了 SLF4J,配置简单、功能强大,是生产环境日志管理的首选方案之一

3. 加密

国产Java工具类库Hutool,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行了封 装,开箱即用

4. TypeHandler

TypeHandler:简单理解就是当处理某些特定字段时,我们可以实现⼀些方法,让Mybatis遇到这些 特定字段可以自动运行处理

4.1 编写一个实体类,凡是此实体类的数据都表示需要加解密的

java 复制代码
@Data
public class Encrypt {
   private String value;

   public Encrypt() {}

   public Encrypt(String value) {
     this.value = value;
   }
}

4.2 编写一个加解密的TypeHandler

@MappedTypes:表示该处理器处理的java类型是什么

@MappedJdbcTypes:表示处理器处理的Jdbc类型

5. Redis的简单使用

Redis 服务器安装在云服务器上,而我们编写的代码则是在本地主机. 要想让本地主机能访问redis,需要把redis的端⼝通过云服务器后台页面的"防火墙"/"安全组"放开 端口到公网上.但是这个操作非常危险(黑客会顺着redis端口进来). 因此我们可以使用端口转发的方式,直接把服务器的redis端口映射到本地.

5.1 测试

java 复制代码
@SpringBootTest
public class RedisTest {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
   
    @Test
    void redisTest(){
        stringRedisTemplate.opsForValue().set("key1","value1");
        System.out.println("从Redis获取值:"+stringRedisTemplate.opsForValue().get("key1"));
    }
}

启动redis service redis-server start

6. RabbitMQ

6.1 什么是MQ

MQ(Message queue),本质是一个队列,FIFO先入先出,只不过队列中存放的内容是消息(message),消息可以非常简单,比如只包含文本的字符串,JSON等,也可以很复杂,比如内嵌对象

MQ多用于分布式系统之间进行通信

系统之间的调用通常有两种方法

  1. 同步通信

直接调用对方的服务,数据从一段发出后立即就可以达到另一端

  1. 异步通信

数据从一段出发后,先进入一个容器进行临时存储,当达到某种条件后,再由这个容器发送给另一端,容器的一个具体实现就是MQ

RabbitMQ 就是MQ的一种实现

6.2 MQ的作用

MQ主要工作是接收并转发消息,在不同的应用场景下可以展现不同的作用

  1. 异步解耦:在业务流程中,一些操作可能非常耗时,但并不需要即使返回结果,可以借助MQ把这些操作异步化,不如用户注册后发送注册短信或邮件通知,可以作为异步处理,不必等待这些操作完成后才告知用户注册成功

  2. 流量削峰:在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见,如果以能处理这类峰值为标准投入资源,无疑是巨大的浪费,使用MQ能够使关键组件支撑突发访问压力,不会因为突发流量而崩溃

  3. 异步通信:在很多时候不需要立即处理消息,MQ提供了异步处理机制,允许应用把一些信息放入MQ中,但并不立即处理它,在需要的时候再慢慢处理

  4. 消息发布:当多个系统需要对同一数据做出响应时,可以使用MQ进行消息分发

  5. 延迟通知:在需要特定时间发布通知的场景中,可以使用MQ的延迟消息功能

  6. ......

6.3 为什么选择RabbitMQ

6.3.1 Kafka

Kafka一开始的目的就是用于日志的收集和传输,追求高吞吐量,性能卓越,单机吞吐达到十万级,在日志领域比较成熟,功能较为简单,主要支持简单的MQ功能,如果有日志采集需求,首选

6.3.2 RocketMQ

RocketMQ采用java语言开发,由阿里巴巴开源,后捐赠给了Apache

它在设计时借鉴了Kafka,并做出了一些自己的改进,青出于蓝而胜于蓝,经过多年双十一的洗礼,在可用性、可靠性以及稳定性方面都有出色的表现,适合对于可靠性要求高,且并发比较大的场景,但支持的客户端语言不多,且社区活跃度一般

6.3.3 RabbitMQ

采用Erlang语言开发,MQ功能比较完备,且几乎支持所有的主流语言,开源提供的界面也十分友好,性能较好,社区活跃度高,比较适合中小型公司,数据量没那么大,且并发没那么高的场景

7. 设计模式

7.1 关于状态转换的常规写法

7.2 问题:

  1. 存在多个处理对象的顺序关系需要维护:奖品+活动状态扭转,活动需要依赖奖品状态改变而改变。可以看出请求依赖于多个决策点,因此处理顺序很重要,不易维护
  2. 需要动态改变算法或行为:是否可以扭转的条件,若将来会发生改变,在这里不易维护
  3. 系统的灵活性和拓展性无法体现
  4. 处理请求的复杂性不易维护

7.3 解决方案:

  1. 策略模式:定义AbstractActivityOperator策略类,和其策略实现类PrizeOperator,ActivityOperator和UserOperator。每个具体的操作类都实现了AbstractActivityOperator定义的接口,代表了不同的状态转换策略
  2. 责任链模式:定义ActivityStatusManager接口类,在ActivityStatusManagerImpl实现中,通过遍历operatorMap中的所有操作符(策略),并按照一定的顺序执行,形成一个责任链,每个操作符判断是否是自己的责任,如果是,则处理请求

责任链模式是一种行为设计模式,它允许将一个请求沿着处理着对象组成的链进行传递。每个处理者对象都有责任去处理请求,或者将它传递给链中的下一个处理者。请求的传递一直进行,知道有一个处理着对象对请求进行了处理,或者知道链的末端仍然未有处理者处理的请求

责任链在项目中的实现

  1. 请求的创建:ConvertActivityStatusDTO 是请求对象,包含了状态转换所需的所有信息
  2. 处理者对象:AbstractActivityOperator及其子类PrizeOperator,ActivityOperator和UserOperator,它们实现了needConvert和convertStatus方法,用以判断是否需要处理请求以及执行处理
  3. 责任链的维护:operatorMap是一个包含所有处理者对象的映射,它按照sequence()方法返回的顺序维护了责任链
  4. 请求的传递:在processStatusConversion方法中,通过迭代器遍历operatorMap,对每个操作符实例调用needConvert方法来判断是否需要由当前操作符处理请求
  5. 处理请求:如果needConvert返回true,则调用convertStatus方法来处理请求
  6. 终止责任链:一旦请求被某个操作符处理,迭代器中的该操作符将被移除,防止请求被重复处理,并且终止了对该操作符的责任链
  7. 异常处理:如果在责任链中的任何点上请求处理失败,则抛出异常,这可以看做是责任链的终止

通过这种方法,责任链模式允许系统在运行时根据请求的类型动态的选择处理者,而不需要修改其他处理者的代码,从而提高了系统的灵活性和可维护性

理解6的移除机制:

1. 副本机制(关键)

代码第一行使用了 Map<String,AbstractActivityOperator> curMap = new HashMap<>(operatorMap);

这里创建的是一个浅拷贝(副本) 。后续所有对 curMap 的增删改,都不会影响原始的 operatorMap,但会影响这个副本本身

2. 第一次调用(处理 sequence = 1)

调用 processConvertStatus(convertActivityStatusDTO, curMap, 1) 时:

  • 迭代器获取了 curMap 的全部元素。

  • 当找到 sequence() == 1 且需要转换的策略时,执行 iterator.remove()

  • Java 迭代器的 remove() 方法会直接从集合(curMap)中物理删除该键值对。

第一次调用结束,此时 curMap所有 sequence = 1 的元素都已经被删干净了

3. 第二次调用(处理 sequence = 2)

调用 processConvertStatus(convertActivityStatusDTO, curMap, 2) 时:

  • 传入的还是同一个 curMap 对象(引用没变)。

  • 虽然循环判断里有 operator.sequence() != sequence,就算不删除,也会因为 sequence=1 != 2continue 跳过它们。

  • 但更关键的是 :由于第一次迭代器的物理删除,第二次调用时,迭代器从头遍历 curMap根本遍历不到 sequence = 1 的对象,因为它们已经不在了。