目录
项目创建
通用功能模块
错误码
自定义异常类
CommonResult
jackson
加密工具
项目创建
使用 idea 创建 SpringBoot 项目,并引入相关依赖:
配置 MyBatis:
编辑 application.yml:
spring:
datasource: # 数据库连接配置
url: jdbc:mysql://127.0.0.1:3306/gobang_system?characterEncoding=utf8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis: # mybatis 配置
configuration:
map-underscore-to-camel-case: true #配置驼峰自动转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句
通用功能模块
通用功能模块 是在软件开发中,创建的一组通用的功能模块,以便在不同的应用场景中重用,从而提高开发效率、降低重复开发工作量,并确保系统的一致性与可维护性。通用功能模块通常具有高度的复用性,能够服务于多个系统或应用
这部分模块通常存放在项目中的 common 包下
错误码
错误码主要用于标识和处理程序运行中的各种异常情况,能够精确的指出问题所在
错误码的作用有:
明确标识错误:错误码提供了一种明确的方式来表示错误的状态,能够精确的指出问题所在
简化问题排查:通过错误码,我们可以快速的定位问题。在系统日志中会包含大量的信息,而错误码作为一种统一的标识符,可以帮助我们在日志中迅速查找特定的错误类型,提高排查效率
错误处理:客户端可以根据错误码进行特定的错误处理,而不是依赖通用的异常处理
易于维护:集中管理错误码使得它们更容易维护和更新。如,业务逻辑变化,只需要更新错误码的定义,而不需要修改每个使用它们的地方。在接口文档中,错误码也可以清晰的列出所有的错误情况,使开发者更容易立即和使用接口
调试和测试:错误码可用于自动化测试,确保特定的错误情况被正确处理
错误分类:错误码可以将错误分类为不同级别或不同类型,如 客户端错误、服务器错误、业务逻辑错误等
创建 errorcode 包:
定义错误码类型:
@Data
public class ErrorCode {
/**
* 错误码
*/
private final Integer code;
/**
* 错误描述信息
*/
private final String message;
public ErrorCode(Integer code, String message) {
this.code = code;
this.message = message;
}
}
定义全局错误码:
public interface GlobalErrorCodeConstants {
// 成功
ErrorCode SUCCESS = new ErrorCode(200, "成功");
// 服务端错误
ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启");
ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "配置项错误");
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
}
定义 controller 层业务错误码:
public interface ControllerErrorCodeConstants {
}
其中的错误码信息随着后续业务代码的完成补充
定义 service 层业务错误码:
public interface ServiceErrorCodeConstants {
}
其中的错误码信息随着后续业务代码的完成补充
自定义异常类
自定义异常类是为了在程序中处理特定的错误或异常情境,使得异常处理更加清晰和灵活。通过自定义异常类,可以根据业务需求定义特定的异常类型,方便捕获和处理特定的错误
创建 exception 包:
controller 层异常类:
@Data
@EqualsAndHashCode(callSuper = true)
public class ControllerException extends RuntimeException {
/**
* controller 层错误码
* @see com.example.gobang_system.common.errorcode.ControllerErrorCodeConstants
*/
private Integer code;
/**
* 错误描述信息
*/
private String message;
/**
* 无参构造方法,方便后续进行序列化
*/
public ControllerException() {}
/**
* 全参构造方法,指定 code 和 message
* @param code
* @param message
*/
public ControllerException(Integer code, String message) {
this.code = code;
this.message = message;
}
/**
* 通过 errorCode 指定 code 和 message
* @param errorCode
*/
public ControllerException(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
}
service 层异常类:
@Data
@EqualsAndHashCode(callSuper = true)
public class ServiceException extends RuntimeException{
/**
* service 层错误码
* @see com.example.gobang_system.common.errorcode.ServiceErrorCodeConstants
*/
private Integer code;
/**
* 错误描述信息
*/
private String message;
/**
* 无参构造方法,方便后续进行序列化
*/
public ServiceException() {}
/**
* 全参构造方法,指定 code 和 message
* @param code
* @param message
*/
public ServiceException(Integer code, String message) {
this.code = code;
this.message = message;
}
/**
* 通过 errorCode 指定 code 和 message
* @param errorCode
*/
public ServiceException(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
}
在进行序列化时需要使用无参构造方法,因此需要提供无参构造方法
那么,在进行序列化时为什么要使用无参构造方法呢?
序列化:将对象转化为字节流
反序列化:从字节流中重建对象
在序列化过程中,使用无参构造方法(即不带任何参数的构造方法)是因为序列化和反序列化涉及将对象的状态转换为字节流 并且再 将其从字节流重建回原对象
序列化过程中,java会将对象的状态(字段值)保存在 字节流 中,而反序列化是通过读取这些字节流恢复对象,当反序列化时,JVM 必须首先 创建一个新的对象实例,然后再将字节流中的数据填充到该对象的字段中。为了能够保证顺利创建对象,java需要一个 无参构造方法 来实例化对象
因此,无参构造方法是反序列化时默认的构造方法,java默认调用该构造方法创建对象实例,因为其没有任何参数,创建对象时无需传递任何参数。如果没有无参构造方法,Java 会试图使用其他构造方法来创建对象,但这些构造方法需要相应的参数传递。而反序列化时,并没有提供参数,这就导致反序列化过程失败
总而言之,无参构造方法在序列化和反序列化中的作用主要体现在以下几个方面:
1. 反序列化需要通过无参构造方法来实例化对象,因为反序列化时无法传递参数给构造方法
2. 无参构造方法保证了对象的正确创建,即使类中有其他的构造方法,也不会影响反序列化的成功
3. 无参构造方法不执行任何业务逻辑,保证了反序列化对象的一致性
@Data 注解
@Data 是 Lombok 提供的一个常见注解,在 java 中用于简化类的代码编写。@Data 注解会为类生成一系列的常用功能代码(自动生成 getter 和 setter 方法、toString 方法等),从而减少代码冗余,提升开发效率
若我们此时运行程序,查看 target 中 的 ControllerException.class:
就可以看到对应的 getter、setter 等方法
@EqualsAndHashCode(callSuper = true)
@EqualsAndHashCode 注解也是 Lombok 中的一个注解,用于自动生成 equals() 和 hashcode() 方法。这两个方法是 Java 中非常常见且重要的方法,通常用于对象的比较和存储在基于哈希表的集合(如 HashMap、HashSet)
callSuper = true:调用父类(super)的 equals() 和 hashCode() 方法,不仅会考虑当前类中的字段,还会考虑父类中的字段,确保父类和子类的字段都参与相等性比较和哈希计算
callSuper = false(默认值):不调用父类的 equals() 和 hashCode() 方法,只考虑当前字段,不考虑父类中的字段
此外,在使用 @Data 注解时,可能会出现反编译 target 文件中并未生成对应 getter、setter 等方法的情况
可能是因为 spring 在创建项目添加 lombok 依赖时,会自动引入一个插件,将其删除即可
更多问题可参考:【SpringBug】lombok插件失效,但是没有报错信息,@Data不能生成get和set方法_lombok data get set-CSDN博客
CommonResult<T>
CommonResult<T> 作为控制层方法的返回类型,封装接口调用结果,包括成功数据、错误数据 和 状态码。它可以被 SpringBoot 框架自动转化为 JSON 或其他格式的响应体,发送给客户端
为什么要进行封装呢?
统一的返回格式:确保客户端收到的响应具有一致的结构,避免每个接口都需要自己定义状态码、消息、数据等内容
错误码和消息:提供错误码(code)和错误消息(errorMessage),帮助客户端快速识别和处理错误
泛型数据返回:使用泛型 <T> 允许返回任何类型的数据,增加了返回对象的灵活性
静态方法:提供了 fail() 和 success() 静态方法,方便快速创建错误或成功的响应对象
错误码常量集成:通过 ErrorCode 和 GlobalErrorCodeConstants 使用预定义的错误码,保持错误码的一致性和可维护性
序列化:实现了 Serializable 接口,使得 CommonResult<T> 对象可以被序列化为多种格式,如 JSON 或 XML,方便网络传输
业务逻辑解耦:将业务逻辑与 API 的响应格式分离,使得后端开发人员可以专注业务逻辑实现,而不必关系如何构建响应
客户端友好:客户端开发人员可以通过统一的接口获取数据和错误信息,无需针对每个 API 编写特定的错误处理逻辑
代码实现:
@Data
public class CommonResult<T> implements Serializable {
/**
* 错误码
* @see ErrorCode#getCode()
*/
private Integer code;
/**
* 返回数据
*/
private T data;
/**
* 错误描述信息
*/
private String errorMessage;
/**
* 业务处理成功
* @param data
* @return
* @param <T>
*/
public static <T> CommonResult<T> success(T data) {
CommonResult result = new CommonResult();
result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
result.data = data;
result.errorMessage = "";
return result;
}
/**
* 业务处理失败
* @param errorCode
* @return
* @param <T>
*/
public static <T> CommonResult<T> fail(ErrorCode errorCode) {
return fail(errorCode.getCode(), errorCode.getMessage());
}
/**
* 业务处理失败
* @param code
* @param errorMessage
* @return
* @param <T>
*/
public static <T> CommonResult<T> fail(Integer code, String errorMessage) {
Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code),
"code = 200, 运行成功");
CommonResult result = new CommonResult();
result.code = code;
result.errorMessage = errorMessage;
return result;
}
}
其中,serializable 接口是 java 提供的一个标记接口(空接口),用于指示一个类的对象可以被序列化,无需实现任何方法,定义在 java.io 包中
此外,若想在 idea 中使用断言,需要先开启断言功能,可参考:
如何开启idea中的断言功能?_idea开启断言-CSDN博客
jackson
在前后端交互的过程中,经常会使用 JSON 格式来传递数据,这也就涉及到 序列化 和 反序列化,此外,我们在进行日志打印时,也会涉及到序列化
因此,我们可以定义一个工具类,来专门处理 序列化
在 java 中,通常使用 ObjectMapper 来处理 Java 对象与 JSON 数据之间的转换
因此,我们首先来学习一下 ObjectMapper 的相关方法和使用
在 test 中创建一个测试类:
@SpringBootTest
public class JacksonTest {
@Test
void jacksonTest() {
}
}
首先来看 object 的序列化:
序列化需要使用 ObjectMapper 中的 writeValueAsString 方法:
处理过程中可能会抛出异常,因此需要进行处理
@SpringBootTest
public class JacksonTest {
@Test
void jacksonTest() {
// 创建 ObjectMapper 实例
ObjectMapper objectMapper = new ObjectMapper();
// 序列化
CommonResult<String> result = CommonResult.success("成功"); // 创建 java 对象
String str = null;
try {
str = objectMapper.writeValueAsString(result);
System.out.println("序列化结果:" + str);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
我们继续看 object 的反序列化:
反序列化需要使用 readValue 方法:
其中,content 是需要读取的字符串,valueType 是将要转化的 java 对象类型
// 反序列化
try {
CommonResult<String> result1 = objectMapper.readValue(str, CommonResult.class);
System.out.println(result1.getCode() + " " + result1.getData());
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
运行并观察结果:
此外,除了处理普通的 object,还可能需要处理一些复杂类型,如 集合、Map 等
例如,处理 List 类型的 序列化 和 反序列化:
List 的序列化 与 object 类型的序列化类似:
// List 的序列化
List<CommonResult<String>> commonResultList = Arrays.asList(
CommonResult.success("test1"),
CommonResult.success("test2"),
CommonResult.success("test3")
);
try {
str = objectMapper.writeValueAsString(commonResultList);
System.out.println(str);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
List 的反序列化:
在对 List 类型进行反序列化时,不能直接将 List 类型传递给 valueType,而是需要构造一个 JavaType 类型
// List 的反序列化
JavaType javaType = objectMapper.getTypeFactory().
constructParametricType(List.class, CommonResult.class); // 构造参数类型
try {
commonResultList = objectMapper.readValue(str, javaType);
for (CommonResult<String> res : commonResultList) {
System.out.println(res.getData());
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
运行并观察结果:
完整测试代码:
@SpringBootTest
public class JacksonTest {
@Test
void jacksonTest() {
// 创建 ObjectMapper 实例
ObjectMapper objectMapper = new ObjectMapper();
// 序列化
CommonResult<String> result = CommonResult.success("成功"); // 创建 java 对象
String str = null;
try {
str = objectMapper.writeValueAsString(result);
System.out.println("序列化结果:" + str);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
// 反序列化
try {
CommonResult<String> result1 = objectMapper.readValue(str, CommonResult.class);
System.out.println(result1.getCode() + " " + result1.getData());
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
// List 的序列化
List<CommonResult<String>> commonResultList = Arrays.asList(
CommonResult.success("test1"),
CommonResult.success("test2"),
CommonResult.success("test3")
);
try {
str = objectMapper.writeValueAsString(commonResultList);
System.out.println(str);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
// List 的反序列化
JavaType javaType = objectMapper.getTypeFactory().
constructParametricType(List.class, CommonResult.class); // 构造参数类型
try {
commonResultList = objectMapper.readValue(str, javaType);
for (CommonResult<String> res : commonResultList) {
System.out.println(res.getData());
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
可以发现,在使用 objectMapper 中的方式时,每次都要对异常进行处理,十分繁琐
那我们该如何简化呢?
我们来看 SpringBoot 框架中是如何实现的:
不同类型的对象序列化是基本相同的,都是使用 writeValueAsString 方法来进行序列化,因此我们主要来看反序列化:
可以看到,反序列化 Map 和 List 都调用了 tryParse 方法,并传递了两个参数:一个 lambda 表达式,一个 Exception
我们继续看 tryParse 方法:
其中,最主要的方法就是 parse.call(),通过 call() 方法,来执行定义的任务
且 tryParse 方法中对异常进行了处理:
check.isAssignableFrom(var4.getClass()) 判断抛出的异常是否是传入的 check 异常,若是,则抛出 JsonParseException 异常;若不是,则抛出 IllegalStateException 异常
可以看到,框架中通过 tryParse() 方法,巧妙地对异常进行了处理
因此,我们可以借鉴上述方法来进行实现
由于只需要使用一个 ObjectMapper 实例,因此可以创建 单例 ObjectMapper:
public class JacksonUtil {
private JacksonUtil() {}
private final static ObjectMapper OBJECT_MAPPER;
static {
OBJECT_MAPPER = new ObjectMapper();
}
private static ObjectMapper getObjectMapper() {
return OBJECT_MAPPER;
}
}
实现 tryParse 方法:
private static <T> T tryParse(Callable<T> parser, Class<? extends Exception> check) {
try {
return parser.call();
} catch (Exception e) {
if (check.isAssignableFrom(e.getClass())) {
throw new JsonParseException(e);
}
throw new IllegalStateException(e);
}
}
private static <T> T tryParse(Callable<T> parser) {
return tryParse(parser, JsonParseException.class);
}
实现序列化方法:
/**
* 序列化
* @param value
* @return
*/
public static String writeValueAsString(Object value) {
return tryParse(() -> getObjectMapper().writeValueAsString(value));
}
反序列化:
/**
* 反序列化
* @param content
* @param valueType
* @return
* @param <T>
*/
public static <T> T readValue(String content, Class<T> valueType) {
return tryParse(() -> {
return getObjectMapper().readValue(content, valueType);
});
}
/**
* 反序列化 List
* @param content
* @param param List 中元素类型
* @return
*/
public static <T> T readListValue(String content, Class<?> param) {
JavaType javaType = getObjectMapper().getTypeFactory()
.constructParametricType(List.class, param);
return tryParse(() -> {
return getObjectMapper().readValue(content, javaType);
});
}
完整代码:
public class JacksonUtil {
private JacksonUtil() {}
private final static ObjectMapper OBJECT_MAPPER;
static {
OBJECT_MAPPER = new ObjectMapper();
}
private static ObjectMapper getObjectMapper() {
return OBJECT_MAPPER;
}
/**
* 序列化
* @param value
* @return
*/
public static String writeValueAsString(Object value) {
return tryParse(() ->
getObjectMapper().writeValueAsString(value));
}
/**
* 反序列化
* @param content
* @param valueType
* @return
* @param <T>
*/
public static <T> T readValue(String content, Class<T> valueType) {
return tryParse(() -> {
return getObjectMapper().readValue(content, valueType);
});
}
/**
* 反序列化 List
* @param content
* @param param List 中元素类型
* @return
*/
public static <T> T readListValue(String content, Class<?> param) {
JavaType javaType = getObjectMapper().getTypeFactory()
.constructParametricType(List.class, param);
return tryParse(() -> {
return getObjectMapper().readValue(content, javaType);
});
}
private static <T> T tryParse(Callable<T> parser, Class<? extends Exception> check) {
try {
return parser.call();
} catch (Exception e) {
if (check.isAssignableFrom(e.getClass())) {
throw new JsonParseException(e);
}
throw new IllegalStateException(e);
}
}
private static <T> T tryParse(Callable<T> parser) {
return tryParse(parser, JsonParseException.class);
}
}
进行测试:
@SpringBootTest
public class JacksonTest {
@Test
void jacksonTest() {
CommonResult<String> failResult = CommonResult.fail(GlobalErrorCodeConstants.ERROR_CONFIGURATION);
// 序列化
String res = JacksonUtil.writeValueAsString(failResult);
System.out.println(res);
// 反序列化
failResult = JacksonUtil.readValue(res, CommonResult.class);
System.out.println(failResult.getCode() + " " + failResult.getErrorMessage());
List<CommonResult<String>> commonResults = Arrays.asList(
CommonResult.success("test1"),
CommonResult.success("test2"),
CommonResult.success("test3")
);
// 序列化 List
String listStr = JacksonUtil.writeValueAsString(commonResults);
System.out.println(listStr);
// 反序列化
commonResults = JacksonUtil.readListValue(listStr, CommonResult.class);
for (CommonResult<String> commonResult: commonResults) {
System.out.println(commonResult.getData());
}
}
}
运行结果:
加密工具
在对敏感信息(如密码、手机号等)进行存储时,需要进行加密,从而保证数据的安全性,若直接明文存储,当黑客入侵数据库时,就可以轻松拿到用户的相关信息,从而造成信息泄露或财产损失
在这里,使用 md5 对用户密码进行加密
采用 判断哈希值是否一致 的方法来判断密码是否正确
详细过程可参考:密码加密及验证_加密算法识别-CSDN博客
完整代码:
public class SecurityUtil {
// 密钥
private static final String AES_KEY = "3416b730f0f244128200c59fd07e6249";
/**
* 使用 md5 对密码进行加密
* @param password 输入的密码
* @return 密码 + 盐值
*/
public static String encipherPassword(String password) {
String salt = UUID.randomUUID().toString().replace("-", "");
String secretPassword = DigestUtils.md5DigestAsHex((password + salt).getBytes());
return secretPassword + salt;
}
/**
* 验证用户输入的密码是否正确
* @param inputPassword 用户输入密码
* @param sqlPassword 数据库中存储密码
* @return
*/
public static Boolean verifyPassword(String inputPassword, String sqlPassword) {
if (!StringUtils.hasLength(inputPassword)) {
return false;
}
if (!StringUtils.hasLength(sqlPassword) || sqlPassword.length() != 64) {
return false;
}
String salt = sqlPassword.substring(32, 64);
String secretPassword = DigestUtils.md5DigestAsHex((inputPassword + salt).getBytes());
return sqlPassword.substring(0, 32).equals(secretPassword);
}
}