
1. 从“Hello World”到“真实世界”SpringBoot项目实战的思维跃迁如果你已经跟着前两天的内容成功跑通了第一个SpringBoot应用看到了那个经典的“Hello World”页面那么恭喜你你已经迈出了坚实的第一步。但我想说的是这仅仅是开始。很多初学者会在这里陷入一个误区以为学会了创建项目、写个Controller返回字符串就算是入门SpringBoot了。实际上从“玩具项目”到“具备生产级雏形的项目”中间隔着一道需要主动跨越的鸿沟。Day03的目标就是带你跨过这道鸿沟不再满足于Demo而是开始用工程化的思维去构建一个“真实”的SpringBoot应用。我们会聚焦于那些在教程里常常一笔带过但在实际开发中天天要打交道的核心配置、集成与部署问题。当你开始思考“我的配置文件该怎么组织”、“日志怎么打才规范”、“如何连接真实的数据库”时你的SpringBoot学习才真正进入了快车道。2. 项目骨架的深化超越默认的application.properties当我们用Spring Initializr创建项目时会得到一个默认的application.properties或application.yml文件。很多人就直接在这里面堆砌所有配置项目稍大一点这个文件就会变得臃肿不堪难以维护。一个清晰的配置管理策略是项目可维护性的基石。2.1 多环境配置让应用适应不同舞台任何严肃的项目都需要区分环境本地开发dev、测试test、预发布staging、生产prod。SpringBoot对此提供了优雅的支持。核心机制ProfileProfile可以理解为一系列配置的“分组”。你可以为每个环境创建独立的配置文件命名规则为application-{profile}.properties/yml。例如application-dev.yml开发环境配置application-test.yml测试环境配置application-prod.yml生产环境配置而通用的、与环境无关的配置则放在主配置文件application.yml中。如何激活特定Profile呢有几种常见方式命令行参数在启动Jar包时指定。这是最灵活、在服务器上最常用的方式。java -jar your-app.jar --spring.profiles.activeprod系统环境变量在服务器上设置SPRING_PROFILES_ACTIVE环境变量。export SPRING_PROFILES_ACTIVEprodIDE配置在IDEA的Run/Debug Configuration中VM options或Program arguments里添加。-Dspring.profiles.activedev 或 --spring.profiles.activedev实操心得配置的优先级与覆盖SpringBoot的配置源是有优先级的。高优先级的配置会覆盖低优先级的。一个典型的优先级顺序从高到低是命令行参数--server.port8081SPRING_APPLICATION_JSON中的属性内嵌的JSON环境变量ServletConfig初始化参数ServletContext初始化参数JNDI属性java:comp/envJava系统属性System.getProperties()操作系统环境变量application-{profile}.yml(带Profile的)application.yml(不带Profile的)Configuration类上的PropertySource注解默认属性通过SpringApplication.setDefaultProperties指定理解这个顺序非常重要。比如生产环境的数据库密码你肯定不想写在代码仓库里的application-prod.yml中。更安全的做法是在application-prod.yml中配置一个占位符然后在服务器上通过环境变量或命令行参数传入真实的密码。这样密码就不会进入代码版本库。2.2 YAML vs Properties不仅仅是语法糖.ymlYAML格式正在逐渐取代.properties这是有原因的。YAML通过缩进来表示层级关系对于复杂结构如列表、嵌套对象的配置其可读性远胜于Properties文件。Properties方式繁琐且易错spring.datasource.urljdbc:mysql://localhost:3306/mydb spring.datasource.usernameroot spring.datasource.password123456 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver spring.datasource.hikari.connection-timeout30000 spring.datasource.hikari.maximum-pool-size10YAML方式清晰直观spring: datasource: url: jdbc:mysql://localhost:3306/mydb username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: connection-timeout: 30000 maximum-pool-size: 10YAML的层级结构一目了然特别是在配置如List或Map时优势更明显。个人建议除非有历史包袱新项目一律使用YAML格式。在IDEA中编写YAML时可以利用其强大的自动补全和语法高亮功能。2.3 自定义配置与ConfigurationProperties我们不仅会读取SpringBoot的配置更多时候需要定义自己的配置。例如定义一个文件上传的路径和大小限制。步骤1在application.yml中定义配置myapp: upload: path: /var/www/uploads/ max-size: 10MB allowed-types: jpg,png,pdf步骤2创建配置属性绑定类强烈推荐使用ConfigurationProperties注解而不是简单的Value。ConfigurationProperties支持类型安全的绑定、验证和宽松的绑定规则如max-size可以绑定到Java字段maxSize。import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Positive; import java.util.List; Component ConfigurationProperties(prefix myapp.upload) // 前缀匹配 public class UploadProperties { NotEmpty private String path; Positive private DataSize maxSize; // 使用Spring的DataSize类型自动转换10MB private ListString allowedTypes; // 标准的getter和setter方法必须提供 public String getPath() { return path; } public void setPath(String path) { this.path path; } // ... 其他getter/setter }步骤3在需要的地方注入使用Service public class FileService { private final UploadProperties uploadProperties; // 通过构造器注入 public FileService(UploadProperties uploadProperties) { this.uploadProperties uploadProperties; } public void handleUpload() { System.out.println(上传路径: uploadProperties.getPath()); System.out.println(最大文件: uploadProperties.getMaxSize().toMegabytes() MB); // 直接使用配置类型安全无需手动解析字符串 } }为什么推荐ConfigurationProperties而不是Value类型安全自动将String转换为Integer、Boolean、DataSize甚至自定义对象。宽松绑定支持kebab-casemax-size、camelCasemaxSize、snake_casemax_size等多种属性名风格自动匹配到Java字段。验证可以结合JSR-303注解如NotEmptyMin在绑定时就进行校验。IDE支持在IDEA中如果你添加了spring-boot-configuration-processor依赖编写YAML时会有自动补全提示极大提升开发体验。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-configuration-processor/artifactId optionaltrue/optional /dependency3. 数据访问层搭建MyBatis-Plus的优雅集成当项目需要持久化数据时我们就要引入数据库。这里我选择介绍MyBatis-Plus因为它是在原生MyBatis基础上做了极大增强的“神器”能让你用极少的代码完成绝大部分CRUD操作同时保留了MyBatis的灵活性。3.1 依赖引入与基础配置首先在pom.xml中添加依赖。注意版本号建议使用与SpringBoot版本兼容的稳定版。dependencies !-- SpringBoot JDBC 支持 (包含HikariCP连接池) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency !-- MySQL驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- MyBatis-Plus 启动器 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.6/version !-- 请使用最新稳定版 -- /dependency /dependencies接着在application.yml中配置数据源。这里以MySQL为例并展示一些HikariCP连接池的优化参数。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/your_database?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: your_username password: your_password hikari: # 连接池名称 pool-name: MyHikariCP # 最小空闲连接数 minimum-idle: 5 # 最大连接数根据应用负载调整一般建议公式CPU核心数 * 2 有效磁盘数 maximum-pool-size: 20 # 连接最大存活时间毫秒MySQL默认wait_timeout是8小时建议略小于此值 max-lifetime: 28740000 # 连接超时时间毫秒 connection-timeout: 30000 # 测试连接有效性的查询语句 connection-test-query: SELECT 13.2 实体、Mapper与Service的极简开发假设我们有一个User用户表。传统的MyBatis需要写XML映射文件而MyBatis-Plus通过注解和继承让这一切变得异常简单。1. 实体类Entity使用TableName指定表名TableId指定主键及策略如自增。import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; // 使用Lombok简化代码 Data TableName(sys_user) public class User { TableId(type IdType.AUTO) // 主键自增 private Long id; private String username; private String email; private Integer age; // 省略 getter/setter (由Lombok的Data注解自动生成) }2. Mapper接口只需继承MyBatis-Plus提供的BaseMapper接口即可获得全套CRUD方法无需编写XML。import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; Mapper // 关键让Spring能扫描到这个Mapper public interface UserMapper extends BaseMapperUser { // 不需要写任何方法BaseMapper已经提供了 // insert, deleteById, updateById, selectById, selectList, selectPage ... // 如果需要复杂查询可以在这里定义方法并配合Select等注解或XML }3. Service层同样可以继承IService和其实现类ServiceImpl获得更强的服务层封装。import com.baomidou.mybatisplus.extension.service.IService; public interface UserService extends IServiceUser { // 可以在此定义业务特有的方法 User getByUsername(String username); }import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; Service public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { Override public User getByUsername(String username) { // 使用MyBatis-Plus的QueryWrapper构建查询条件 QueryWrapperUser queryWrapper new QueryWrapper(); queryWrapper.eq(username, username); // getOne 方法获取一条记录 return this.getOne(queryWrapper); } }4. Controller层调用import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/users) public class UserController { Autowired private UserService userService; // 分页查询 GetMapping public PageUser list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { PageUser page new Page(pageNum, pageSize); return userService.page(page); } // 条件查询 GetMapping(/search) public ListUser search(RequestParam String keyword) { QueryWrapperUser wrapper new QueryWrapper(); wrapper.like(username, keyword).or().like(email, keyword); return userService.list(wrapper); } // 新增 PostMapping public Boolean save(RequestBody User user) { return userService.save(user); } // 更新、删除等操作类似... }踩坑实录Mapper注解与扫描路径如果你在Controller里注入Mapper时遇到NoSuchBeanDefinitionException最常见的原因有两个忘记在Mapper接口上添加Mapper注解。或者你更倾向于使用包扫描。可以在主启动类上添加MapperScan(com.yourpackage.mapper)注解指定Mapper接口所在的包路径。这样该包下的所有Mapper接口都会被自动注册为Bean无需每个接口都加Mapper。个人建议使用MapperScan更清晰统一。3.3 高级特性分页插件与逻辑删除MyBatis-Plus的插件机制非常强大只需简单配置即可启用高级功能。分页插件配置在配置类中或主启动类配置分页插件否则Page对象不会生效。import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 还可以添加其他插件如乐观锁插件 // interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }配置后在Service中调用page(Page page, Wrapper wrapper)方法即可实现物理分页SQL会自动添加LIMIT语句。逻辑删除逻辑删除是指用字段标记记录被删除而非物理删除数据。MyBatis-Plus对此有开箱即用的支持。在表中添加一个删除标记字段如deleted类型为tinyint或int默认值为0。在实体类中对应字段上添加TableLogic注解。Data TableName(sys_user) public class User { // ... 其他字段 TableLogic private Integer deleted; // 0-未删除1-已删除 }在application.yml中配置逻辑删除的全局值可选新版通常不需要。mybatis-plus: global-config: db-config: logic-delete-field: deleted # 全局逻辑删除的实体字段名 logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值配置完成后当你调用userService.removeById(1)时执行的不再是DELETE FROM user WHERE id1而是UPDATE user SET deleted1 WHERE id1 AND deleted0。所有的select操作也会自动加上AND deleted0条件。这极大地简化了逻辑删除的实现。4. 应用日志SLF4J与Logback的实战配置日志是应用的“眼睛”生产环境排查问题离不开它。SpringBoot默认使用Logback作为日志框架并通过SLF4J门面进行调用。但默认配置可能不符合生产要求我们需要进行定制。4.1 理解日志级别与门面模式日志级别从低到高分为TRACEDEBUGINFOWARNERROR。设置某个级别后只会打印该级别及更高级别的日志。开发环境可以设为DEBUG生产环境通常设为INFO或WARN。SLF4JSimple Logging Facade for Java是一个日志门面它不负责具体的日志实现只是提供统一的API。你的代码里应该只调用SLF4J的接口如LoggerFactory.getLogger。这样底层无论是用Logback、Log4j2还是其他实现都可以无缝切换。SpringBoot默认的Starter已经帮你做好了桥接。4.2 定制Logback配置文件SpringBoot允许在resources目录下放置一个logback-spring.xml文件来覆盖默认配置。使用-spring后缀可以让SpringBoot识别并应用其特有的扩展如springProfile。一个功能齐全的生产级logback-spring.xml配置示例?xml version1.0 encodingUTF-8? configuration scantrue scanPeriod60 seconds !-- 定义变量 -- property nameLOG_HOME value./logs/ property nameAPP_NAME valuemy-springboot-app/ property nameLOG_PATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/ !-- 控制台输出 (开发环境常用) -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 按天滚动的文件输出 (生产环境必备) -- appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/${APP_NAME}.log/file !-- 滚动策略按日期文件大小 -- rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy !-- 每日滚动并保留30天历史文件格式为 my-app-2024-01-01.log -- fileNamePattern${LOG_HOME}/${APP_NAME}-%d{yyyy-MM-dd}.%i.log/fileNamePattern timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP !-- 每个日志文件最大100MB -- maxFileSize100MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy maxHistory30/maxHistory !-- 保留30天的日志 -- /rollingPolicy encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 错误日志单独输出到一个文件 -- appender nameERROR_FILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/${APP_NAME}-error.log/file filter classch.qos.logback.classic.filter.ThresholdFilter levelERROR/level /filter rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePattern${LOG_HOME}/${APP_NAME}-error-%d{yyyy-MM-dd}.log/fileNamePattern maxHistory30/maxHistory /rollingPolicy encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 根据不同Profile激活不同配置 -- springProfile namedev root levelDEBUG appender-ref refCONSOLE/ /root !-- 将特定包如Mapper的日志级别调低方便查看SQL -- logger namecom.yourpackage.mapper levelDEBUG/ /springProfile springProfile nameprod root levelINFO appender-ref refFILE/ appender-ref refERROR_FILE/ !-- 生产环境也可以输出到控制台方便容器采集 -- appender-ref refCONSOLE/ /root !-- 生产环境关闭一些不必要或过于详细的日志 -- logger nameorg.springframework levelWARN/ logger namecom.zaxxer.hikari levelINFO/ /springProfile /configuration关键配置解读RollingFileAppender这是核心确保日志文件不会无限增大。通过TimeBasedRollingPolicy实现按天归档结合SizeAndTimeBasedFNATP实现按天按大小滚动例如每天一个文件但单个文件超过100MB就切分。maxHistory设置保留的历史日志文件天数防止磁盘被撑满。ThresholdFilter用于ERROR_FILE只记录ERROR及以上级别的日志方便错误排查。springProfile这是SpringBoot的扩展可以根据激活的Profile使用不同的日志策略。开发环境侧重控制台输出和DEBUG信息生产环境侧重文件输出、性能和信息安全。4.3 在代码中优雅地记录日志不要在代码里直接System.out.println使用SLF4J的API。import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.RestController; RestController public class SomeController { // 使用当前类作为Logger的名称这是标准做法 private static final Logger log LoggerFactory.getLogger(SomeController.class); public void someMethod() { // 使用占位符{}避免字符串拼接在日志级别不够时拼接操作是浪费的 String userId 123; log.info(用户 [{}] 尝试登录, userId); // 输出用户 [123] 尝试登录 try { // 一些可能出错的业务 riskyOperation(); } catch (Exception e) { // 记录错误时一定要将异常对象作为最后一个参数传入 log.error(处理用户 [{}] 请求时发生异常, userId, e); // 这样日志框架才能打印完整的堆栈跟踪信息 } // 对于需要复杂计算才能得到的日志信息先判断级别避免无效计算 if (log.isDebugEnabled()) { log.debug(一些复杂的调试信息: {}, expensiveCalculation()); } } private String expensiveCalculation() { // 模拟一个耗时的计算 return 计算结果; } }实操心得日志级别的选择ERROR系统发生了错误必须有人立即处理。例如数据库连接失败、核心服务调用异常。WARN不应该发生但暂时不影响系统运行需要关注。例如缓存失效回退到数据库、API调用超时但重试成功。INFO重要的业务流程节点信息。例如用户登录成功、订单创建完成。生产环境的标准输出级别。DEBUG调试信息用于开发阶段定位问题。例如详细的SQL参数、方法入参出参。TRACE最详细的跟踪信息通常只在排查极端问题时开启。5. 应用打包与部署从JAR到Docker开发完成的SpringBoot应用最终需要部署到服务器上运行。SpringBoot的“打包即运行”特性让部署变得非常简单。5.1 打包可执行JAR与瘦身使用Maven或Gradle可以轻松打包。在项目根目录执行mvn clean package命令执行后会在target目录下生成一个your-app-0.0.1-SNAPSHOT.jar文件。这个JAR是可执行的因为它内嵌了Tomcat等Web容器如果是Web应用。直接运行即可java -jar target/your-app-0.0.1-SNAPSHOT.jarJAR包瘦身问题默认打包方式spring-boot-maven-plugin会生成一个“胖JAR”Fat Jar它包含了所有依赖库和你的应用代码。这可能导致JAR包很大几十MB甚至上百MB。每次更新代码即使只改了一行也需要上传整个大JAR包效率低下。解决方案依赖分离打包可以将依赖库lib和你的应用代码classes分开打包。修改pom.xml中的插件配置build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration !-- 启用分层打包为Docker镜像优化做准备 -- layers enabledtrue/enabled /layers !-- 或者使用更传统的依赖分离 -- !-- executabletrue/executable layoutZIP/layout -- /configuration executions execution goals goalrepackage/goal /goals /execution /executions /plugin /plugins /build更彻底的分离方式是使用maven-dependency-plugin将依赖JAR复制到target/lib目录然后修改启动脚本的类路径。但SpringBoot的layers特性是更现代、更推荐的方式尤其配合Docker时。5.2 使用Docker容器化部署Docker是目前部署应用的事实标准。它为应用提供了一个一致的运行环境。1. 编写Dockerfile在项目根目录创建Dockerfile# 第一阶段构建 # 使用包含Maven的官方镜像来构建应用 FROM maven:3.8.7-eclipse-temurin-17 AS builder WORKDIR /app # 复制pom.xml和源代码 COPY pom.xml . COPY src ./src # 构建应用跳过测试 RUN mvn clean package -DskipTests # 第二阶段运行 # 使用更小的JRE基础镜像而不是完整的JDK FROM eclipse-temurin:17-jre-alpine WORKDIR /app # 从构建阶段复制构建产物 COPY --frombuilder /app/target/*.jar app.jar # 创建一个非root用户运行应用增强安全性 RUN addgroup -S spring adduser -S spring -G spring USER spring:spring # 暴露端口与application.yml中server.port一致 EXPOSE 8080 # 启动应用通过环境变量传递Profile等配置 ENTRYPOINT [java, -jar, /app/app.jar]2. 构建Docker镜像在Dockerfile所在目录执行docker build -t my-springboot-app:latest .3. 运行容器# 简单运行 docker run -p 8080:8080 my-springboot-app:latest # 更贴近生产的运行方式设置环境变量、挂载日志卷、使用自定义网络 docker run -d \ --name my-app \ --network my-bridge-network \ -p 8080:8080 \ -e SPRING_PROFILES_ACTIVEprod \ -e JAVA_OPTS-Xms512m -Xmx1024m \ -v /host/path/logs:/app/logs \ my-springboot-app:latest关键参数解释-d后台运行。--network将容器加入自定义网络方便容器间通信如连接数据库容器。-e设置环境变量。这里设置了激活prodProfile并传递JVM内存参数。-v将宿主机的目录挂载到容器内。这里将日志目录挂载出来这样即使容器被删除日志文件还在宿主机上。5.3 关于“信创”与TongWeb的思考在提供的热词中有一个问题“改成信创的话是否需要东方通的tongweb”。这是一个非常实际的问题。核心结论通常不需要。SpringBoot应用打包成的可执行JAR其内嵌的Web容器默认是Tomcat已经是一个完整的、符合Servlet规范的Web服务器。它可以直接在符合Java运行环境的信创操作系统如麒麟、统信UOS和信创硬件如鲲鹏、飞腾上运行无需额外安装像TongWeb、金蝶Apusic、中创InforSuite这样的传统国产中间件。传统中间件如TongWeb的角色更像是一个“应用服务器”用于部署传统的WAR包。你需要将应用打成WAR然后部署到TongWeb的webapps目录下由TongWeb来提供Servlet容器环境。SpringBoot的“内嵌容器”模式已经把这个容器Tomcat, Jetty, Undertow打包进了JAR里。它自己就是自己的“中间件”。因此在信创环境下部署SpringBoot JAR包只需要确保目标服务器安装了兼容的JRE如龙芯JDK、毕昇JDK等信创版Java。直接运行java -jar your-app.jar。什么情况下会用到TongWeb如果你的项目是传统的、非SpringBoot的Java Web应用打成了WAR包。如果企业有统一的技术栈规划强制要求所有Java应用必须部署在TongWeb上。如果需要用到TongWeb提供的一些特定企业级功能而这些功能是内嵌Tomcat没有的不过这种情况较少SpringBoot的生态通常能覆盖。所以对于全新的SpringBoot项目在信创环境下优先采用内嵌容器直接运行JAR包的方式架构更简单运维也更方便。