3. 新建项目
3.1 选择相应的框架
pom文件配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>lottery_system</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>lottery_system</name>
<description>lottery_system</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<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>
</project>
3.2 代码结构设计
分层结构如下:
4. 功能模块设计
4.1 错误码
我们对dao层不进行定义错误码;
对controller层和servoce层进行定义错误码;
对于一个全局的错误码,成功,失败,未知;
代码如下:
package com.example.lotterysystem.common;
/**
* @version 1.0
* @Author 作者名
* @Date 2024/11/24 19:55
* @注释
*/
public interface GlobalErrorCodeConstants {
ErrorCode SUCCESS = new ErrorCode(200,"成功!");
ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500,"系统异常!");
ErrorCode UNKNOW = new ErrorCode(101,"未知错误!");
}
4.2 ⾃定义异常类
根据控制层和服务层来分别写相应的代码,其实两部分的代码都相近,同时引入了@EqualsAndHashCode(callSuper = true)注解:
@Data
@EqualsAndHashCode(callSuper = true)
public class ControllerException extends RuntimeException{
/**
* @see com.example.lotterysystem.common.errcode.ControllerErrorCodeConstants
*/
private int code;//异常吗
private String message;//异常消息
//为了序列化
public ControllerException(){
}
public ControllerException(Integer code,String message){
this.code = code;
this.message = message;
}
public ControllerException(ErrorCode errorCode){
this.code = errorCode.getCode();
this.message= errorCode.getMessage();
}
}
4.3. CommonResult<T>
CommonResult<T>作为控制器层⽅法的返回类型,封装HTTP接⼝调⽤的结果,包括成功数据、错 误信息和状态码。它可以被SpringBoot框架等⾃动转换为JSON或其他格式的响应体,发送给客⼾端。
关于为什么要封装?
1. 统⼀的返回格式:确保客⼾端收到的响应具有⼀致的结构,⽆论处理的是哪个业务逻辑。 2. 错误码和消息:提供错误码(code)和错误消息(msg),帮助客⼾端快速识别和处理错误。
3. 泛型数据返回:使⽤泛型允许返回任何类型的数据,增加了返回对象的灵活性。
4. 静态⽅法:提供了error()和success()静态⽅法,⽅便快速创建错误或成功的响应对象。
5. 错误码常量集成:通过ErrorCode和GlobalErrorCodeConstants使⽤预定义的错误码,保持错 误码的⼀致性和可维护性。
6. 序列化:实现了Serializable接⼝,使得CommonResult对象可以被序列化为多种格式,如 JSON或XML,⽅便⽹络传输。
7. 业务逻辑解耦:将业务逻辑与API的响应格式分离,使得后端开发者可以专注于业务逻辑的实 现,⽽不必关⼼如何构建HTTP响应。
8. 客⼾端友好:客⼾端开发者可以通过统⼀的接⼝获取数据和错误信息,⽆需针对每个API编写特 定的错误处理逻辑。
@Data
//因为时进行http传输,为了成功传输就要进行序列化
public class CommonResult<T> implements Serializable {
private Integer code;
private T data;
private String message;
public static <T> CommonResult<T> success(T data){
CommonResult<T> result = new CommonResult<>();
result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
result.data = data;
result.message = "";
return result;
}
public static <T> CommonResult<T> error(Integer code,String message){
Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code),
"code不是错误的异常");
CommonResult<T> result = new CommonResult<>();
result.code = code;
result.message = message;
return result;
}
public static <T> CommonResult<T> error(ErrorCode errorCode){
return error(errorCode.getCode(),errorCode.getMessage());
}
}
4.4 jackson工具实现序列化
相对而言protobuf(pb实现序列化非常快,但是可视化非常差);
public class JacksonUtil {
private JacksonUtil(){
}
private final static ObjectMapper OBJECT_MAPPER;//1点对objectmapper进行私有化单例操作
static {
OBJECT_MAPPER = new ObjectMapper();
}
private static ObjectMapper getObjectMapper(){
return OBJECT_MAPPER;
}
//2点。使用callable支持使用lamda表达式来进行处理,无论序列化的操作过程中出现的
//异常都可以tryparse进行捕捉
private static <T > T tryParse(Callable<T> parser) {
return tryParse(parser, JacksonException.class);
}
private static <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);
}
throw new IllegalStateException(ex);
}
}
/**
*序列化objectmapper
* @param value
* @return
*/
public static String writeValueAsString(Object value) {
return JacksonUtil.tryParse(()->
JacksonUtil.getObjectMapper().writeValueAsString(value));
}
/**
反序列化objectmapper
* @param content
* @param valueType
* @return
* @param <T>
*/
public static <T > T readValue(String content, Class<T> valueType) {
return JacksonUtil.tryParse(()->
JacksonUtil.getObjectMapper().readValue(content, valueType));
}
/**
反序列化list
* @param content
* @param parameterClasses
* @return
* @param <T>
*/
public static <T > T readListValue(String content, Class<?> parameterClasses) {
JavaType javaType = JacksonUtil.getObjectMapper().getTypeFactory().constructParametricType(List.class, parameterClasses);
return JacksonUtil.tryParse(()->
{ return JacksonUtil.getObjectMapper().readValue(content,javaType);
});
}
}
4.5. ⽇志处理
使用SLF4J+logback的配置
在本地(dev),通过控制台来打印出日志;
服务器:日志主要存在目标目录文件;
application.properties配置:
spring.application.name=lottery_system
## logback xml ##
logging.config=classpath:logback-spring.xml
spring.profiles.active=dev
新增logback-spring.xml文件:
对于本地和服务器的日志进行配置:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<springProfile name="dev">
<!--输出到控制台-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n%ex</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console" />
</root>
</springProfile>
<springProfile name="prod,test">
<!--ERROR级别的日志放在logErrorDir目录下,INFO级别的日志放在logInfoDir目录下-->
<property name="logback.logErrorDir" value="/root/lottery-system/logs/error"/>
<property name="logback.logInfoDir" value="/root/lottery-system/logs/info"/>
<property name="logback.appName" value="lotterySystem"/>
<contextName>${logback.appName}</contextName>
<!--ERROR级别的日志配置如下-->
<appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则
如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天
的日志改名为今天的日期。即,<File> 的日志都是当天的。
-->
<File>${logback.logErrorDir}/error.log</File>
<!-- 日志level过滤器,保证error.***.log中只记录ERROR级别的日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!--滚动策略,按照时间滚动 TimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
<FileNamePattern>${logback.logErrorDir}/error.%d{yyyy-MM-dd}.log</FileNamePattern>
<!--只保留最近30天的日志-->
<maxHistory>30</maxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<!--<totalSizeCap>1GB</totalSizeCap>-->
</rollingPolicy>
<!--日志输出编码格式化-->
<encoder>
<charset>UTF-8</charset>
<pattern>%d [%thread] %-5level %logger{36} %line - %msg%n%ex</pattern>
</encoder>
</appender>
<!--INFO级别的日志配置如下-->
<appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则
如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天
的日志改名为今天的日期。即,<File> 的日志都是当天的。
-->
<File>${logback.logInfoDir}/info.log</File>
<!--自定义过滤器,保证info.***.log中只打印INFO级别的日志, 填写全限定路径-->
<filter class="com.example.lotterysystem.common.filter.InfoLevelFilter"/>
<!--滚动策略,按照时间滚动 TimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
<FileNamePattern>${logback.logInfoDir}/info.%d{yyyy-MM-dd}.log</FileNamePattern>
<!--只保留最近14天的日志-->
<maxHistory>14</maxHistory>
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<!--<totalSizeCap>1GB</totalSizeCap>-->
</rollingPolicy>
<!--日志输出编码格式化-->
<encoder>
<charset>UTF-8</charset>
<pattern>%d [%thread] %-5level %logger{36} %line - %msg%n%ex</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="fileErrorLog" />
<appender-ref ref="fileInfoLog"/>
</root>
</springProfile>
</configuration>
在配置的包中自定义过滤info级别日志的类:
public class InfoLevelFilter extends Filter<ILoggingEvent> {
//过滤器来过滤info级别的日志,将info的日志进行接受;
@Override
public FilterReply decide(ILoggingEvent iLoggingEvent) {
if (iLoggingEvent.getLevel().toInt() == Level.INFO.toInt()){
return FilterReply.ACCEPT;
}
return FilterReply.DENY;
}
}
使用测试类来进行日志打印测试:
ps:未完待续