news 2026/7/9 9:29:10

从零开始学SpringBoot:一个完整项目开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零开始学SpringBoot:一个完整项目开发实战

写代码这件事,最难的不是语法,不是框架,而是那个从0到1的瞬间。当你面对着空白的IDEA界面,不知道该点哪里,不知道该创建什么文件夹,甚至连一个能跑的Hello World都敲不出来的时候,那种无力感会直接劝退90%的人。SpringBoot号称“开箱即用”,但这个箱子怎么开,门朝哪边,没人告诉你。

今天这篇文章,我会直接带你走一遍实战流程,从一个全新的空项目开始,一直到构建出一个具备用户管理、数据交互、异常处理、日志监控、单元测试的全栈项目。这不是那种只有几个注解的“假项目”,而是一个真正可以上线的骨架。

环境准备

在开始之前,我必须强调一个原则:不要用最新版本。很多人一上来就去Spring官网下载最新的3.x版本,结果发现一堆库不兼容,连Maven仓库都找不到对应依赖。这不是你的问题,是生态适配永远滞后于版本发布。建议你用 SpringBoot 2.7.x 版本,稳定,资料多,社区活跃度最高。JDK选择11或17,不要碰JDK 21,除非你做好了当小白鼠的准备。构建工具选Maven,别碰Gradle。Gradle虽然语法更简洁,但是对于初学者来说,Gradle的配置陷阱能让你的调试时间翻三倍

安装好IntelliJ IDEA,不要用社区版。社区版没有Spring Initializr,这会让你的起步变得异常痛苦。如果你实在没钱买正版,去官网下载Ultimate版试用30天,这30天足够你完成学习并判断自己是否需要继续投入。

第一次启动项目

打开IDEA,选择New Project -> Spring Initializr。这里要注意:不要直接点Next到底。Group填你自己的域名倒写,比如com.zhangsan。Artifact填项目名,比如user-centerType选Maven POM,Language选Java,Packaging选Jar。Java Version选11。然后点Next,选择依赖:Spring Web、Spring Data JPA、MySQL Driver、Lombok、Spring Boot DevTools。

生成项目后,你会看到如下目录结构:

user-center/ ├── src/ │ ├── main/ │ │ ├── java/com/zhangsan/usercenter/ │ │ │ ├── UserCenterApplication.java │ │ │ └── ... │ │ └── resources/ │ │ ├── application.properties │ │ └── ... │ └── test/ └── pom.xml

现在,别急着写代码。先去检查pom.xml里的SpringBoot版本。如果你选的是2.7.x,那没问题。如果你是手动创建的,直接在<parent>标签里写死版本:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent>

这是你整篇代码里最重要的几行之一。SpringBoot的版本锁定机制,决定了你所有依赖的兼容性。版本号写错,后面所有的坑都会从这里长出来。

第一个能跑的API

删掉自动生成的测试代码。我们要从最原始的Controller开始。程序员对框架的信心,永远来自一个能跑通的API。没有这个,后面所有的配置都是空中楼阁。

创建包结构:controllerservicemodelrepository。然后在controller包下建一个HelloController.java

@RestController @RequestMapping("/api/v1") public class HelloController { @GetMapping("/hello") public String sayHello() { return "你好,SpringBoot!"; } }

启动项目。右键UserCenterApplication.java,点Run。打开浏览器输入http://localhost:8080/api/v1/hello。看到页面显示“你好,SpringBoot!”——恭喜你,你迈出了最难的一步。但请注意:你现在做的只是“启动了一个嵌入式Tomcat”,离真正的项目还差得很远

配置文件的正确打开方式

application.properties是SpringBoot的命脉。不要在这文件里写一堆没用的注释。我们需要配置真实环境:

server.port=8080 spring.datasource.url=jdbc:mysql://localhost:3306/user_center?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true spring.datasource.username=root spring.datasource.password=你的密码 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

你必须明白ddl-auto=update的真实含义:Hibernate会根据你定义的实体类自动建表、改表。开发阶段用update非常方便,但上了生产环境必须改成none或者手动管理,否则一个误操作直接删表。show-sql=true在开发时能看到执行的SQL语句,排查问题效率飞起。

建表与实体映射

在数据库中创建数据库user_center。然后在model包下创建User.java实体类:

@Entity @Table(name = "users") @Data @NoArgsConstructor @AllArgsConstructor public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true, length = 50) private String username; @Column(nullable = false) private String password; @Column(nullable = false, unique = true) private String email; @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } }

这里的@PrePersist@PreUpdateJPA生命周期回调注解,用来自动填充时间字段。很多人直接手动设置时间,然后发现数据库里全是null。你必须在实体层面保证时间字段的自动处理,这是好习惯。@Column注解里的nullableunique这些约束,不是摆设。它们直接决定数据库表结构是否正确。建表时没加唯一约束,代码里判断再多次也拦不住脏数据。

数据访问层

创建UserRepository.javarepository包下:

@Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); Optional<User> findByEmail(String email); Boolean existsByUsername(String username); Boolean existsByEmail(String email); }

Spring Data JPA会根据方法名自动生成SQL实现findByUsername会自动生成select from users where username = ?。你不需要写一行SQL。但前提是字段名必须和实体类中的属性名严格对应。这里最容易犯的错误是:实体类里字段叫username,你写findByName,然后一直报错找不到数据。调试半小时发现是命名不对。

existsByUsername直接返回布尔值,常用于注册时的重名检测,比查出来再判断性能好得多。

业务逻辑层

service包下创建UserService.java

@Service @Transactional public class UserService { @Autowired private UserRepository userRepository; @Autowired private PasswordEncoder passwordEncoder; public User registerUser(UserDto userDto) { if (userRepository.existsByUsername(userDto.getUsername())) { throw new RuntimeException("用户名已存在"); } if (userRepository.existsByEmail(userDto.getEmail())) { throw new RuntimeException("邮箱已被注册"); } User user = new User(); user.setUsername(userDto.getUsername()); user.setPassword(passwordEncoder.encode(userDto.getPassword())); user.setEmail(userDto.getEmail()); return userRepository.save(user); } public User getUserByUsername(String username) { return userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("用户不存在")); } }

@Transactional确保整个方法在一个数据库事务中执行。如果save失败,之前的所有操作(包括存在性检查)都会被回滚。没有这个注解,你可能会遇到数据部分写入的诡异问题。PasswordEncoder是Spring Security的组件,后面会引入。现在先声明,保证代码结构完整。

这里有一个非常常见的认知误区:不要直接在Controller里调用Repository。很多人为了省事,在Controller里直接注入UserRepository,然后调findAll。短期看确实快,但项目一旦复杂起来,多一个表关联、多一个缓存逻辑,就只能在Controller里堆屎山。分层是架构的灵魂

统一返回体与异常处理

大多数初学者都会犯一个错误:Controller里用Map来返回结果。用Map是最丑陋的代码形态之一,不做类型安全,可读性差,前端对接时全靠猜。

创建统一的API响应类ApiResponse.java

@Data @AllArgsConstructor @NoArgsConstructor public class ApiResponse<T> { private int code; private String message; private T data; public static <T> ApiResponse<T> success(T data) { return new ApiResponse<>(200, "成功", data); } public static <T> ApiResponse<T> error(int code, String message) { return new ApiResponse<>(code, message, null); } }

再创建一个全局异常处理器GlobalExceptionHandler.java

@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public ApiResponse<?> handleRuntimeException(RuntimeException ex) { return ApiResponse.error(400, ex.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public ApiResponse<?> handleValidationException(MethodArgumentNotValidException ex) { String msg = ex.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(", ")); return ApiResponse.error(400, msg); } @ExceptionHandler(Exception.class) public ApiResponse<?> handleException(Exception ex) { return ApiResponse.error(500, "服务器内部错误"); } }

@RestControllerAdvice是SpringBoot最强大的功能之一。你可以在这里集中处理所有异常,而不是在每个Controller里写try-catch。MethodArgumentNotValidException专门处理参数校验失败的情况。有了这个,前端传进来的数据不合法时,系统不会崩掉,而是返回清晰的提示信息。

Controller层的完整实现

现在改写UserController

@RestController @RequestMapping("/api/v1/users") @Validated public class UserController { @Autowired private UserService userService; @PostMapping("/register") public ApiResponse<User> register(@Valid @RequestBody UserDto userDto) { User user = userService.registerUser(userDto); return ApiResponse.success(user); } @GetMapping("/{username}") public ApiResponse<User> getUser(@PathVariable String username) { User user = userService.getUserByUsername(username); return ApiResponse.success(user); } @GetMapping("/list") public ApiResponse<List<User>> listUsers() { List<User> users = userService.getAllUsers(); return ApiResponse.success(users); } @PutMapping("/{id}") public ApiResponse<User> updateUser(@PathVariable Long id, @Valid @RequestBody UserDto userDto) { User user = userService.updateUser(id, userDto); return ApiResponse.success(user); } @DeleteMapping("/{id}") public ApiResponse<Void> deleteUser(@PathVariable Long id) { userService.deleteUser(id); return ApiResponse.success(null); } }

注意这里的@ValidBean Validation(JSR-380)是必学的技能,它让你用注解来定义参数规则,而不是写一堆if语句。对应的UserDto要加上校验注解:

@Data public class UserDto { @NotBlank(message = "用户名不能为空") @Size(min = 3, max = 20, message = "用户名长度需要在3-20之间") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, message = "密码长度不能少于6位") private String password; @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; }

校验注解带来的好处远超你的想象。一个@NotBlank同时做了空字符串、null和全空格的校验。你手动写if至少需要三行代码。@Email直接帮你验证邮箱格式,不用手动写正则。

引入Spring Security和JWT安全机制

没有安全的系统就是裸奔的网站。先把密码加密做上。在pom.xml里加依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>

然后配置SecurityConfig.java

@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/v1/users/register", "/api/v1/auth/login").permitAll() .anyRequest().authenticated() .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } }

BCryptPasswordEncoder是目前最强的主流密码加密算法,加密强度自动随算力提升而增强。csrf().disable()在REST API场景下是安全的,因为前端通常是SPA应用,CSRF token对他们毫无意义。STATELESS告诉Spring Security不用创建session,所有的状态通过JWT传递。

JWT令牌生成与验证

创建JwtUtil.java

@Component public class JwtUtil { private static final String SECRET_KEY = "your-256-bit-secret-you-should-store-in-environment-variable"; private static final long EXPIRATION_TIME = 86400000; // 24小时 public String generateToken(String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public String extractUsername(String token) { return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getSubject(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } }

JWT的核心是签名signWith(HS256, SECRET_KEY)用密钥对payload进行哈希,任何人篡改token都会导致验签失败。EXPIRATION_TIME设为24小时是合理的,太短了用户频繁登录体验差,太长了有安全风险。

再写JwtAuthFilter.java拦截请求:

@Component public class JwtAuthFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); if (jwtUtil.validateToken(token)) { String username = jwtUtil.extractUsername(token); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>()); SecurityContextHolder.getContext().setAuthentication(authentication); } } chain.doFilter(request, response); } }

OncePerFilterd确保每个请求只被执行一次。filter链的注册需要在SecurityConfig中完成,加上.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)。这一步很多人漏掉,导致filter永远不生效。

前端模板集成Thymeleaf

SpringBoot也支持服务端渲染。在pom.xml里加依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>

resources/templates下创建index.html

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>用户管理系统</title> </head> <body> <h1 th:text="'欢迎来到用户管理系统,' + ${username} + '!'"></h1> </body> </html>

Thymeleaf的最大优势在于“静态文件可直接用浏览器打开预览”th:text是模板引擎的属性,在服务端渲染阶段会替换成真实数据,但直接打开HTML文件时显示的是默认文字,不影响前端调试。

创建ViewController.java

@Controller public class ViewController { @GetMapping("/") public String index(Model model) { model.addAttribute("username", "游客"); return "index"; } }

注意这里是@Controller不是@RestController@RestController会自动对每个方法应用@ResponseBody,导致返回的是字符串,而不是视图模板。这是SpringBoot新手最容易踩的坑之一。

日志配置

application.properties里加:

logging.level.com.zhangsan.usercenter=DEBUG logging.file.name=logs/app.log logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n logging.file.max-size=10MB logging.file.max-history=7

永远不要依赖System.out.println来调试。在微服务架构里,日志是唯一的调试手段。设置max-sizemax-history是为了防止日志文件撑爆磁盘。线上环境建议再引入Logstash或ELK体系,把日志集中收集。

在关键业务方法里加日志

@PostMapping("/register") public ApiResponse<User> register(@Valid @RequestBody UserDto userDto) { log.info("收到注册请求: {}", userDto.getUsername()); User user = userService.registerUser(userDto); log.info("用户注册成功: {}", user.getId()); return ApiResponse.success(user); }

日志级别要合理使用:INFO记录业务流程,DEBUG记录变量细节,WARN记录非预期但可恢复的情况,ERROR记录必须人工介入的异常。

单元测试

SpringBoot对测试的支持非常强大。@SpringBootTest可以自动注入完整的应用上下文。

写一个测试类:

@SpringBootTest @AutoConfigureMockMvc class UserControllerTest { @Autowired private MockMvc mockMvc; @Test void testRegister() throws Exception { String userJson = "{\"username\":\"testuser\",\"password\":\"test123\",\"email\":\"test@example.com\"}"; mockMvc.perform(post("/api/v1/users/register") .contentType(MediaType.APPLICATION_JSON) .content(userJson)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.username").value("testuser")); } @Test void testRegisterWithDuplicateUsername() throws Exception { String userJson = "{\"username\":\"testuser\",\"password\":\"test123\",\"email\":\"test@example.com\"}"; mockMvc.perform(post("/api/v1/users/register") .contentType(MediaType.APPLICATION_JSON) .content(userJson)) .andExpect(status().isOk()); mockMvc.perform(post("/api/v1/users/register") .contentType(MediaType.APPLICATION_JSON) .content(userJson)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(400)) .andExpect(jsonPath("$.message").value("用户名已存在")); } }

MockMvc是测试REST API的神器,它不会启动真实服务器,而是模拟HTTP请求,速度快,不依赖端口。jsonPath可以精确校验返回体中的字段值,比字符串匹配更可靠。

测试覆盖率不是越高越好,但关键路径必须有测试。用户注册、登录、数据不存在时的异常处理,这些核心逻辑被覆盖了,项目才有安全感。

打包与部署

在项目根目录下执行mvn clean package -DskipTests,会生成target/user-center-0.0.1-SNAPSHOT.jar瘦身打包很重要:SpringBoot默认打包会把所有依赖打进一个fat jar,体积大,上传慢。可以加spring-boot-maven-plugin配置,实现分层构建:

<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <layers> <enabled>true</enabled> </layers> </configuration> </plugin>

分层之后,业务代码和依赖库分开,上传jar包时只更新业务代码层,增量部署高效得多。

部署命令:java -jar user-center-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod。通过--spring.profiles.active=prod指定生产环境的配置文件application-prod.properties,把数据库地址、密码、日志级别等敏感信息与开发环境隔离。

不要在生产环境上用update模式。线上数据库表结构变更必须先走数据库迁移脚本(Flyway或Liquibase),这是底线。

总结与进阶

从零开始学SpringBoot,真正难的不是框架本身,而是建立完整的上下文。你知道怎么配数据库了,你知道什么是JPA的懒加载和级联了,你知道JWT的工作原理了,你知道怎么用MockMvc做自动化测试了——这些东西单独拎出来都不难,难的是把它们整合在一个项目中,让它们协同工作。

写代码不是背API,是构建思维模型。当你脑子里能跑通一个完整的请求流程,从浏览器到Controller到Service到Repository到数据库再返回来,你才真正掌握了SpringBoot。

如果你已经看到这里,并且按步骤把项目跑起来了,那你已经超过了80%的初学者。接下来,你可以继续研究:

集成Redis做缓存,解决热点数据性能问题

集成Spring Cloud Alibaba,学习Nacos做服务注册与发现

引入消息队列RabbitMQ,处理异步任务

深入学习JPA的复杂查询,包括规格查询和子查询

SpringBoot的世界很大,但你已经拿到了钥匙。接下来的路,就是每天写一个接口,每天解决一个bug,日积月累,你终将成为那个在深夜解决问题、让系统稳定运行的架构师。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/9 9:28:53

SGLang推理框架思路到原型:快速验证AI论文中的工程可行性

SGLang推理框架思路到原型&#xff1a;快速验证AI论文中的工程可行性 一、从论文到原型&#xff1a;AI创业公司的技术验证困境 2024年AI论文发表数量超过23万篇&#xff0c;平均每天新增630篇。某AI创业公司技术团队尝试复现一篇关于"结构化生成加速"的论文&#xff…

作者头像 李华
网站建设 2026/7/9 9:26:03

计算机毕业设计之老龄化社区服务管理系统的设计与实现

系统根据现有的管理模块进行开发和扩展&#xff0c;采用面向对象的开发的思想和结构化的开发方法对老龄化社区服务管理的现状进行系统调查。采用结构化的分析设计&#xff0c;该方法要求结合一定的图表&#xff0c;在模块化的基础上进行系统的开发工作。在设计中采用“自下而上…

作者头像 李华
网站建设 2026/7/9 9:25:26

终极i茅台自动预约系统:一站式解决方案告别手动抢购烦恼

终极i茅台自动预约系统&#xff1a;一站式解决方案告别手动抢购烦恼 【免费下载链接】campus-imaotai i茅台app自动预约&#xff0c;每日自动预约&#xff0c;支持docker一键部署&#xff08;本项目不提供成品&#xff0c;使用的是已淘汰的算法&#xff09; 项目地址: https:…

作者头像 李华
网站建设 2026/7/9 9:22:07

HTML初相识:从零搭建网页结构

大家好&#xff0c;这里是你的HTML入门笔记。今天我们来拆解一下网页的骨架——HTML&#xff0c;掌握最基础的结构和标签知识。一、HTML文档的基本骨架一个标准的HTML文档&#xff0c;就像盖房子的地基和框架。在VS Code等编辑器中&#xff0c;输入 !后按 Tab或 Shift !可以快…

作者头像 李华
网站建设 2026/7/9 9:22:02

日常居家小酌选什么养生酒?复盛公六味地黄酒口感解析

家人们&#xff0c;今天来给大家分享一下日常居家小酌选什么养生酒&#xff0c;顺便解析一下复盛公六味地黄酒的口感。首先&#xff0c;选养生酒得看原料。复盛公六味地黄酒就做得很棒&#xff0c;复盛公集团从源头把控品质&#xff0c;甄选地黄、山茱萸、山药、肉苁蓉、黄精、…

作者头像 李华