【项目】抽奖系统bug历程(持续更新)

1. 构建错误码

在 common 层中,设置错误码

2. 构建自定义异常

注意事项:

  1. 继承自 RuntimeException
  2. @EqualsAndHashCode(callSuper = true)// 使用父类的equals和hashcode方法,不使用lombok生成的

3. 构建统一结果返回

common 层 中,设置统一结果返回(CommonResult)

注意事项:

  1. 使用泛型的格式:

    复制代码
     public static <T> CommonResult<T> error(Integer code, String msg) {
  2. Jackson 在进行序列化时,一般需要类有无参构造函数,并且属性要有对应的 getter 方法。

记得加上 @Data

默认的 lombok 依赖会出现错误:

复制代码
<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>// 加上版本号
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.24</version>// 加上版本号
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

在 Java 中,方法返回类型前的 <T>**不能省略 ,因为它是泛型方法的类型参数声明** ,用于告诉编译器:"这个方法使用一个名为T的泛型类型"。如果省略<T>,编译器会将T视为一个实际的类(如java.lang.Object的子类),而不是泛型类型参数,从而导致编译错误。

3.1.1. 返回类型前的<T> 为什么不能省略?

3.1.1.1. (1)语法规则

泛型方法的类型参数声明必须紧跟在方法修饰符(如publicstatic)之后,返回类型之前。省略<T>会导致语法错误:java

复制代码
// 错误:缺少类型参数声明,编译器无法识别T
public void printArray(T[] array) { ... }  // 报错:Cannot resolve symbol 'T'

// 正确:声明泛型参数T
public <T> void printArray(T[] array) { ... }
3.1.1.2. (2)类型推断机制
  • 若省略<T>,编译器会认为T是一个已存在的具体类型(如类或接口),而非泛型类型参数。
  • 例如,若代码中没有定义名为T的类,编译器会报错 "无法解析符号 'T'"。

3.1.2. 泛型类型擦除机制

在 Java 中,泛型类型参数 T****在运行时必须被具体类型替换或擦除,否则会导致编译错误或运行时异常。

也就是说,运行时,不能存在还不知道替换成什么具体类型的T。

示例:

复制代码
public static <T> T readValue(String content, T valueType) {
        return JacksonUtil.tryParse(()->{
           return JacksonUtil.getObjectMapper().readValue(content, valueType);
        });
    }

这段代码种的声明部分,存在运行时还未知的 T,所以错误!

更正:

复制代码
public static <T> T readValue(String content, Class<T> valueType) {
        return JacksonUtil.tryParse(()->{
           return JacksonUtil.getObjectMapper().readValue(content, valueType);
        });
    }
3.1.2.1. 为什么是Class<T>,而不是class<T>?

在 Java 中,Class 和 class 的区别源于大小写的不同,这实际上代表了两种完全不同的语法概念:

  1. ClassJava 中的类型字面量

Class 是 Java 中的一个内置类,用于表示类的运行时类型信息(RTTI)。

泛型形式:Class 中的 T 表示该 Class 对象所代表的实际类型。

  1. class<T>****:Java 中的语法错误
  • class 是 Java 的关键字,用于定义类(如 public class MyClass {})。
  • 泛型类定义 :应使用 class MyClass<T> {},但不能单独作为类型使用。

Class 就像是 class 的类。

4. 构建序列化工具

工具有:

  1. fastjson
  2. jackson(选中)
  3. protobuf

可视化差、但是速度快。

演示 List 的:

复制代码
/**
         * List序列化
         */
        List<CommonResult<String>> list = Arrays.asList(
                CommonResult.success("success1"),
                CommonResult.success("success2")
        );
        String s1;
        try {
            s1 = objectMapper.writeValueAsString(list);
            System.out.println("List序列化:" + s1);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }


        /**
         * List反序列化
         */
        JavaType javaType = objectMapper.getTypeFactory()
                .constructParametricType(List.class, CommonResult.class);
        try {
            List<CommonResult<String>> o = objectMapper.readValue(s1, javaType);
            System.out.println(Arrays.toString(new List[]{o}));
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }

创建一个表示List<T>JavaType对象,其中TparameterClasses指定。


一直 try-catch,冗余,学习 spring 框架中是怎么处理的:

check.isAssignableFrom(ex.getClass())作用:

判断 ex 是否是 check 的实例/子类


5. 日志配置

使用 slf4j & logback。

yml 文件:

复制代码
logging:
  config: classpath:logback-spring.xml # logback日志配置
  level:
    root: INFO

日志级别:

  • DEBUG ➜ 非常详细(开发排查阶段用)
  • INFO ➜ 常规运行信息(生产环境建议使用)
  • WARN ➜ 警告
  • ERROR ➜ 错误

6. 加密信息

  1. 工具: 使用 Hutool。
  2. 方式:使用加盐 + Hash 加密。

6.1. 加密方式

6.1.1. 对称加密

可以还原,适合大量数据加密。

常用算法

  • AES(高级加密标准):支持 128/192/256 位密钥长度,安全性高且性能优异,被广泛应用于金融、通信领域。
  • DES/3DES:早期对称加密算法,DES 因密钥较短(56 位)已不安全,3DES 通过三重加密增强安全性,但效率较低。

6.1.2. 非对称加密(Hash 加密)

不可以还原。

常用算法

  • SHA-256:安全哈希算法,输出 256 位哈希值,广泛用于密码加密、数字签名等场景。
  • BCrypt:专门设计用于密码存储的哈希算法,自带盐值(Salt)和自适应哈希难度,抗暴力破解能力强。
  • Scrypt:基于内存硬计算的哈希算法,对硬件资源要求高,适合防御 GPU/ASIC 加速破解

6.2. 手机号加密

中奖后需要手机号发送中奖信息,选择 aes 对称加密。

6.3. 密码加密

不需要知道用户的密码信息,选择 sha-256 非对称加密。

7. 用户注册

构建分层:

|-------------------|-----------|----------------------------------|
| controller | param | 接收上层的查询参数 |
| controller | result | 为前端返回结果 |
| controller | convert | 转换 service 返回的结果为 controller 层格式 |
| service(接口实现分离写法) | dto | service 层向外传输数据据类 |
| service(接口实现分离写法) | impl | service 接口具体实现 |
| service(接口实现分离写法) | interface | 提供业务逻辑处理接口 |

7.1. 为什么进行接口分离设计?

接口与实现的分离是Java 编程中推崇的一种设计哲学,它有助于创建更加灵活、可维护和可扩展的软件系统。

  1. 抽象与具体实现分离:接口定义了一组操作的契约,而实现则提供了这些操作的具体行为。这种分离允许改变具体实现而不影响使用接口的客户端代码。
  2. 支持多态性:接口允许通过共同的接口来引用不同的实现,这是多态性的基础,使得代码更加灵2.活和通用。
  3. 提高代码的可读性和可理解性:接口提供了清晰的 AP! 视图,使得其他开发者能够更容易地理解和使用这些 API。
  4. 安全性:接口可以隐藏实现细节,只暴露必要的操作,这有助于保护系统的内部状态和实现不被4.
  5. 外部直接访问。
  6. 遵循开闭原则:软件实体应当对扩展开放,对修改封闭。接口与实现的分离使得在不修改客户端代码的情况下扩展系统的功能。
  7. 促进面向对象的设计:接口与实现的分离鼓励开发者进行面向对象的设计,考虑如何将系统分解6:为可重用和可组合的组件。

7.2. 校验

使用 springboot 自带的 validation 包提供的 @NotBlank

分类:

如果是容器@NotEmpty

如果是普通对象@NotBlank

如果是Object@NotNull

8. 校验邮箱、手机、密码和身份类别是否合规

8.1. 正则表达式

ai 出表达式,封装好工具类。

8.2. HuTool

使用 HuTool 也可完成功能,在 core 包中。

8.3. 手机号

加密和解密的这个过程,需要不断进行,所以需要使用 TypeHandler

其能够将指定的类型转换为数据库的类型。

因为不是所有 String 都需要进行加密(例如邮箱),所以需要一个Encrypt类包装 String。

这个类只有一个属性,就是 String value。

复制代码
public class Encrypt {
    private String value;
}

8.4. 密码

8.4.1. 判空逻辑:

在 Java 开发中,使用!StringUtils.hasText(param.getPassword())而非 param.getPassword() == null 来判断字符串是否为空,主要基于以下几个核心原因:

一、对 ** 空值(null)和空字符串("")** 的全面判断

  1. param.getPassword() == null 的局限性
    仅能判断是否为 null,无法处理以下情况:
    字符串为 ""(空字符串):例如用户输入密码时直接提交空值。
    字符串为全空格(如 " "):某些业务场景下需排除这种无效输入。
  2. StringUtils.hasText() 的优势
    同时处理三种 "空" 状态:
    null:对象未初始化或未赋值。
    "":空字符串。
    " ":仅包含空格、制表符等空白字符的字符串。

8.5. 注意事项

校验身份类别的时候,需要使用枚举类,enum 的使用注意:

  1. 实例必须写在最前面
  2. 构造方法默认是 private 的
  3. 获取全部枚举实例:xxx.values()

8.5.1. 注入相关

测试的时候,service 的 impl 类在 controller 层使用了 new 进行使用,导致后面在加入 mapper 的时候,无法注入。

具体表现为:

测试类中是正常的,但是 service 的 impl 中就是找不到这个 mapper 的 bean。

将 controller 中的 impl 交由 spring 进行管理即可!(接口不需要注入)

8.5.2. 序列化

在 Java 中,序列化机制要求类必须具备无参构造函数,这是由 Java 序列化的底层实现原理决定的。以下是详细解释和例外情况说明:

一、为什么需要无参构造函数?

  1. 反序列化的本质
    当对象被反序列化时(如从字节流恢复对象),Java 虚拟机需要通过反射机制创建类的实例。此时,Java 会默认调用类的无参构造函数来初始化对象。
    如果类中没有显式定义无参构造函数,且存在其他有参构造函数,编译器不会自动生成无参构造函数,导致反序列化时因无法找到构造函数而抛出 InvalidClassException。

  2. 示例:无参构造函数缺失导致的错误

    import java.io.Serializable;

    // 错误:未定义无参构造函数,且存在有参构造函数
    public class User implements Serializable {
    private String name;
    private int age;

    复制代码
     // 有参构造函数(无无参构造函数)
     public User(String name, int age) {
         this.name = name;
         this.age = age;
     }
    
     // 反序列化时会报错:no valid constructor found

    }

9. 发送短信验证码

个人资质暂时不能通过审核,mock。

  1. 使用 redis 缓存验证码。

10. 进行验证码验证

使用 HuTool。

直接 cv。

11. 用户登录

缺点:

  1. 只能 web 使用(客户端没有这个机制)
  2. 集群环境下 cookie 不共享,无法使用
  3. 不能跨域,跨域不认识 cookie

11.2. Token 验证

缺点:

  1. 用户数量大 -> 内存压力大

11.3. 基于 Token 的 JWT 令牌

  1. JWT 存储在前端
  2. 加密解密依赖 JWT 内部操作

11.4. 校验流程

  1. web 传给后端用户信息
  2. 后端对于验证码、密码等用户信息进行校验,生成 JWT 令牌发送给前端
  3. 前端存储 JWT 令牌,访问的时候带着 JWT 令牌
相关推荐
一点.点17 分钟前
针对Python开发的工具推荐及分析,涵盖集成开发环境(IDE)、轻量级工具、在线开发平台、代码管理工具等)
开发语言·ide·python·开发工具
老李不敲代码18 分钟前
JavaScript性能优化实战大纲
开发语言·javascript·性能优化
一个天蝎座 白勺 程序猿24 分钟前
Python爬虫(40)基于Selenium与ScrapyRT构建高并发动态网页爬虫架构:原理、实现与性能优化
爬虫·python·selenium
明月看潮生1 小时前
青少年编程与数学 02-020 C#程序设计基础 06课题、运算符和表达式
开发语言·青少年编程·c#·编程与数学
androidwork1 小时前
Kotlin全栈工程师转型路径
android·开发语言·kotlin
不二狗4 小时前
每日算法 -【Swift 算法】实现回文数判断!
开发语言·算法·swift
csdn_aspnet5 小时前
Java 程序求圆弧段的面积(Program to find area of a Circular Segment)
java·开发语言
佩奇的技术笔记5 小时前
Python入门手册:Python中的数据结构类型
数据结构·python
进击的_鹏6 小时前
【C++】红黑树的实现
开发语言·c++
梁下轻语的秋缘7 小时前
Python人工智能算法 模拟退火算法求解01背包问题:从理论到实践的完整攻略
人工智能·python·算法·数学建模·模拟退火算法