Bootstrap

java抽奖系统(二)

 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:未完待续

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;