适合Java新手练手的SpringBoot博客系统:含前台展示、后台管理与基础框架模块
简介:这个SpringBoot博客项目专为刚学完Java基础和SpringBoot入门的学习者设计,包含三个核心模块:sangeng-blog(用户浏览的博客前台)、sangeng-admin(管理员使用的后台系统)、sangeng-framework(可复用的基础功能模块)。项目采用前后端分离结构,前端页面完整,后端接口清晰,支持文章发布、分类管理、评论互动、用户登录等常见博客功能。所有代码基于标准Maven结构组织,pom.xml依赖配置齐全,.gitignore文件已预置,README.md提供基础运行指引,可直接导入IDEA或Eclipse一键启动。目录结构体现典型的企业级分层思想——controller层处理请求、service层封装逻辑、mapper层对接数据库、entity和dto负责数据传输,方便理解模块化开发流程。配套代码注释较充分,适合边跑边读、边改边学,是巩固SpringBoot核心组件(如Spring MVC、MyBatis Plus、Spring Security、JWT)实战能力的实用练手素材。
1. 为什么这个SpringBoot博客项目,真能带新手“踩进坑里再爬出来”
我带过几十个刚学完Java基础、正对着SpringBoot官方文档发懵的新人。他们最常问的问题不是“怎么写Controller”,而是:“我照着教程写了,但启动报错,连首页都打不开,该看哪一行日志?”——这种问题,光靠理论讲不清,必须亲手把项目跑起来,在真实报错里摸清SpringBoot的脾气。
这个叫sangeng-blog的项目,名字听起来平平无奇,但它不是那种“Hello World式”的玩具Demo。它用三个物理隔离的Maven模块(sangeng-blog、sangeng-admin、sangeng-framework),把一个真实博客系统拆解成了可触摸、可调试、可打断点的实体。你导入IDEA后看到的不是一坨src/main/java堆在一起的代码,而是三个清晰的工程节点:左边是用户刷的首页和文章页,中间是管理员登录后点来点去的后台面板,右边是一个写着“framework”的灰色模块,里面全是工具类、统一返回封装、全局异常处理器——这恰恰是企业里最常被忽略、却最影响开发效率的“地基”。
关键词里写的“前后端分离”,在这里不是一句空话。sangeng-blog模块的pom.xml里压根没引入Thymeleaf或JSP依赖,它的职责就是对外暴露RESTful接口;而sangeng-admin模块也一样,它不渲染页面,只提供/admin/api/开头的管理接口;真正的HTML、CSS、JS全在前端工程里(虽然项目包里没直接放,但README里明确写了“前端使用Vue2+ElementUI,源码见GitHub同名仓库”)。这意味着你第一次启动时,会遇到一个典型困惑:“我启动了sangeng-blog,浏览器访问localhost:8080显示404?”——答案很简单:它本就不该返回HTML,它只响应/api/请求,比如/api/article/list。这个“预期落差”,恰恰是理解前后端分离的第一课:后端不是“做网页”的,是“提供数据服务”的。
更关键的是,它把新手最容易卡壳的几个技术点,都放在了“伸手就能摸到”的位置:登录用JWT而不是Session,密码加密用BCrypt而不是明文存库,分页查询用MyBatis-Plus的Page对象而不是手写limit语句,权限控制用Spring Security的注解@PreAuthorize而不是自己if-else判断角色。这些不是炫技,而是当前Java生态里最主流、最稳妥的实践。你改一行密码加密逻辑,立刻能看到数据库里存的密文变了;你删掉一个@PreAuthorize注解,马上发现未授权用户能访问删除接口——这种即时反馈,比读十页文档都管用。
所以,别把它当成“练手项目”,就当它是你第一个要上线的小型SaaS产品。你会为它配Nginx反向代理,会调MySQL慢查询日志,会在sangeng-framework里加一个自定义的Excel导出工具类。它不完美:前端没打包进后端jar、没有Dockerfile、Redis缓存只做了文章列表预热……但正是这些“不完美”,给你留出了真实的改造空间。接下来,我就带你一层层拆开它的骨架,告诉你每个模块为什么这么设计、哪些地方藏着新手必踩的坑、以及怎么从“能跑起来”进化到“能改明白”。
2. 项目整体架构与模块化设计逻辑拆解
2.1 三层物理模块:不是为了炫技,而是为了“看得见”的职责分离
整个项目用Maven多模块结构组织,这是企业级Java项目的标配,但很多新手一看到pom.xml里嵌套的 就头皮发麻。其实它的核心逻辑非常朴素: 把不同人干的活,放进不同的文件夹里,避免互相污染。
-
sangeng-blog模块:这是面向终端用户的“前台”。它的src/main/java下只有controller、service、mapper三层,没有一个HTML文件,也没有任何前端资源。它的pom.xml里最关键的依赖是:
xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.4</version> </dependency>
注意:它没有spring-boot-starter-thymeleaf,没有spring-boot-starter-freemarker。这意味着它天生就是API服务,强制你接受“前端是独立工程”的事实。当你在浏览器里输入http://localhost:8080/api/article/list,它返回的是JSON数据,而不是渲染好的网页。这种“拒绝妥协”的设计,反而帮你绕过了“前后端混合开发”的认知陷阱。 -
sangeng-admin模块:这是给管理员用的“后台”。它的结构和sangeng-blog几乎一致,但路径前缀和权限逻辑完全不同。它的Controller类上基本都加了
@RequestMapping("/admin"),所有接口URL自动带上/admin前缀。更重要的是,它的pom.xml里多了两样东西: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.2</version> </dependency>
Spring Security负责拦截未登录请求,JWT负责生成和校验token。这里有个新手常忽略的细节:sangeng-admin的SecurityConfig配置类里,antMatchers("/admin/login").permitAll()放行了登录接口,但antMatchers("/admin/**").authenticated()要求其他所有/admin路径都必须携带有效token。这个“放行登录、保护其余”的模式,是所有基于Token的权限系统的基石。 -
sangeng-framework模块:这是整个项目的“工具箱”。它不处理业务,只提供通用能力。打开它的src/main/java目录,你会看到:
com.sangeng.framework.config:全局WebMvcConfigurer(解决跨域)、统一返回Result类、全局异常处理器GlobalExceptionHandler;com.sangeng.framework.util:JWT工具类(生成token、解析token、校验token过期)、Redis缓存工具类、MD5/BCrypt密码工具;com.sangeng.framework.aspect:日志切面(记录每个Controller方法的入参、耗时、结果)。
这个模块的pom.xml里没有任何业务依赖,只有spring-boot-starter和lombok。它的存在意义在于:当sangeng-blog和sangeng-admin都需要发邮件、都需要操作Redis、都需要统一返回格式时,你不用在两个模块里各写一遍,而是让它们都依赖sangeng-framework。这就是“复用”的真实模样——不是抽象成高大上的设计模式,而是把重复代码抽成一个jar包。
提示:新手常犯的错误是试图在sangeng-framework里写业务Service。记住:framework模块里永远不该出现ArticleService、CategoryService这类业务类。它的边界很清晰——只做“所有业务都需要的、和具体业务无关的事”。
2.2 为什么坚持前后端分离?一次HTTP请求的完整旅程
很多人觉得“前后端分离”就是前端用Vue、后端用SpringBoot,然后用axios调接口。这没错,但没抓住本质。本质是关注点分离:前端工程师只关心“怎么把数据漂亮地展示出来”,后端工程师只关心“怎么把数据准确、安全、高效地查出来”。
我们以用户访问一篇博客文章为例,走一遍完整的链路:
- 用户在浏览器地址栏输入
https://blog.example.com/article/123; - 前端Vue路由捕获这个URL,触发
ArticleDetail.vue组件的mounted()钩子; - 组件内执行
this.$http.get('/api/article/123'),发起一个GET请求到后端; - 请求到达sangeng-blog模块的
ArticleController,其方法签名是:java @GetMapping("/article/{id}") public Result<ArticleVo> getArticleById(@PathVariable Long id) { ... } - Controller调用
articleService.getById(id),Service层查数据库,Mapper层执行SQL; - 数据查出来后,Controller不返回HTML,而是封装成
Result<ArticleVo>对象,其中ArticleVo是专门给前端用的视图对象(可能包含文章内容、作者昵称、分类名称、评论数等,字段比数据库实体Article更丰富); - SpringBoot的
@RestController自动将Result对象序列化为JSON,通过HTTP响应体返回给前端; - Vue拿到JSON后,用v-model绑定到模板里,完成渲染。
这个过程中,后端完全不知道前端用的是Vue还是React,甚至不知道页面长什么样。它只认一个契约:/api/article/{id}这个URL,必须返回一个特定结构的JSON。这种松耦合,让前后端可以并行开发——前端用Mock.js模拟接口数据,后端专注写业务逻辑,最后联调时只要JSON结构对得上,就能跑通。
注意:项目里的
LIkDr87tsVPNje32Zdwj-master-3047f0f7284f80142d22fb60070f3c90bbd2fa2e这个奇怪命名的目录,其实是Git克隆时产生的冲突文件(.gitignore.hoist-conflict-),属于历史遗留垃圾,务必在导入IDEA前手动删除*。否则Maven会尝试编译它,导致莫名其妙的ClassNotFoundException。
2.3 模块间依赖关系:一张图看懂谁靠谁活着
三个模块不是平等的兄弟关系,而是有明确的父子依赖:
sangeng-blog ──┬── 依赖 sangeng-framework
└── 不依赖 sangeng-admin
sangeng-admin ──┬── 依赖 sangeng-framework
└── 不依赖 sangeng-blog
sangeng-framework ──┬── 不依赖 sangeng-blog
└── 不依赖 sangeng-admin
这种单向依赖(A→B表示A依赖B)是模块化设计的黄金法则。它保证了:
- 修改sangeng-framework里的JWT工具类,sangeng-blog和sangeng-admin都会生效,无需分别修改;
- 如果你想把sangeng-blog改成用MongoDB,只需替换它的MyBatis-Plus依赖,不影响sangeng-framework里的Redis工具;
- 当你需要给后台增加“文章审核”功能时,只在sangeng-admin里加Controller和Service,sangeng-blog完全不受影响。
在父pom.xml里,你可以看到这样的声明:
<modules>
<module>sangeng-framework</module>
<module>sangeng-blog</module>
<module>sangeng-admin</module>
</modules>
注意sangeng-framework排在第一位。Maven构建时,会先编译framework,再编译blog和admin(因为它们依赖framework)。如果你不小心把顺序写反,比如把blog放在framework前面,Maven就会报错:“无法解析依赖sangeng-framework”,因为它还没编译出来。
3. 核心功能模块详解与实操要点
3.1 用户前台(sangeng-blog):从首页到文章详情的全流程实现
sangeng-blog模块是用户感知最直接的部分,它的核心任务是:把数据库里的文章、分类、标签,变成前端能消费的JSON数据。我们以“首页文章列表”功能为例,拆解它的完整实现链路。
第一步:数据库设计与MyBatis-Plus映射
项目使用MySQL,核心表有sg_article(文章)、sg_category(分类)、sg_comment(评论)。sg_article表的关键字段包括:
- id: BIGINT 主键
- title: VARCHAR(100) 文章标题
- content: TEXT 文章内容(富文本HTML)
- category_id: BIGINT 分类ID(外键关联sg_category)
- create_time: DATETIME 创建时间
- is_top: TINYINT(1) 是否置顶(0否1是)
对应的Java实体类Article.java在sangeng-blog/src/main/java/com/sangeng/blog/entity下:
@TableId(type = IdType.ASSIGN_ID)
public class Article {
private Long id;
private String title;
private String content;
private Long categoryId;
private LocalDateTime createTime;
private Integer isTop;
// getter/setter省略
}
注意@TableId(type = IdType.ASSIGN_ID):这不是用数据库自增,而是MyBatis-Plus的雪花算法ID生成器,确保分布式环境下ID不重复。新手常误以为@TableId只是标记主键,其实type参数决定了ID如何生成——如果删掉这个注解,插入数据时id会是null,导致SQL异常。
第二步:Mapper接口与XML(或注解)查询
ArticleMapper.java继承BaseMapper<Article>,获得通用CRUD方法。但首页需要“按创建时间倒序、分页、且只查已发布(status=0)的文章”,这就需要自定义方法:
public interface ArticleMapper extends BaseMapper<Article> {
// 方式1:用@Select注解写原生SQL
@Select("SELECT * FROM sg_article WHERE status = 0 ORDER BY create_time DESC LIMIT #{offset}, #{limit}")
List<Article> selectPublishedList(@Param("offset") long offset, @Param("limit") long limit);
// 方式2:用XML(推荐,SQL更清晰)
List<Article> selectPublishedListWithXml(@Param("page") Page<Article> page);
}
项目采用方式2,在ArticleMapper.xml里写:
<select id="selectPublishedListWithXml" resultType="com.sangeng.blog.entity.Article">
SELECT * FROM sg_article
WHERE status = 0
ORDER BY create_time DESC
</select>
这里的关键是:MyBatis-Plus的Page对象会自动注入LIMIT #{page.offset}, #{page.size},你不用手动拼分页SQL。page.offset由前端传来的当前页码计算得出(如第1页offset=0,第2页offset=10),page.size是每页条数(如10)。
第三步:Service层组装业务逻辑
ArticleServiceImpl.java里,getArticleList()方法不是简单调Mapper,而是要做三件事:
1. 调用Mapper查出原始Article列表;
2. 遍历列表,为每篇文章查出它的分类名称(categoryMapper.selectById(article.getCategoryId()));
3. 将原始Article和分类名称组装成ArticleVo(View Object),返回给Controller。
为什么不能直接在Mapper里用JOIN查分类名称?因为ArticleVo还需要其他字段,比如“评论数”。如果在SQL里LEFT JOIN sg_comment表,会导致一条文章对应多条评论,结果集行数爆炸(1篇文章+5条评论=5行结果)。所以必须在Service层用N+1查询(1次查文章,N次查分类),再用ArticleVo聚合数据。这是新手最容易写出性能问题的地方——看到JOIN就用,却不考虑数据膨胀。
第四步:Controller统一返回与异常处理
ArticleController.java的方法签名是:
@GetMapping("/article/list")
public Result<PageVo<ArticleVo>> getArticleList(PageVo<ArticleVo> pageVo) {
return articleService.getArticleList(pageVo);
}
注意返回类型是Result<PageVo<ArticleVo>>,不是PageVo<ArticleVo>。Result是sangeng-framework里定义的统一返回类,结构为:
public class Result<T> {
private Integer code; // 状态码,200成功,500失败
private String msg; // 提示信息
private T data; // 具体数据
}
这样做的好处是:前端无论成功还是失败,都收到相同结构的JSON,方便统一处理。比如登录失败时,后端返回{"code":401,"msg":"未授权","data":null},前端直接toast提示,不用写两套解析逻辑。
实操心得:新手常把业务异常(如“文章不存在”)和系统异常(如“数据库连接超时”)混为一谈。项目里用
GlobalExceptionHandler统一捕获RuntimeException,但对“文章不存在”这种业务场景,应该抛出自定义异常ArticleNotFoundException,并在Exception Handler里单独处理,返回code=404。否则所有错误都返回500,前端无法区分是用户操作问题还是服务器问题。
3.2 后台管理(sangeng-admin):登录认证与权限控制的落地细节
sangeng-admin模块的核心挑战是:如何让一个HTTP请求,既知道你是谁,又知道你能干什么。它用Spring Security + JWT实现了这套机制,我们拆解登录和接口鉴权两个关键环节。
登录流程:从密码输入到Token生成
- 前端提交用户名密码到
/admin/login接口; LoginController.java接收请求,调用loginService.login(username, password);LoginServiceImpl.java中:
- 根据username查用户(userMapper.selectByUsername(username));
- 用BCrypt工具类校验密码:BCrypt.checkpw(password, user.getPassword());
- 校验通过后,生成JWT Token:java String token = JwtUtil.createToken( Map.of("id", user.getId(), "username", user.getUsername()), 24 * 60 * 60 * 1000L // 有效期24小时 );JwtUtil.createToken()方法在sangeng-framework里,它用HS256算法,把用户ID和用户名作为payload,加上密钥(如"sangeng-secret-key")签名,生成一串base64编码的字符串,如eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiJ9.xxxx;- 将token放入Result返回给前端,前端存储在localStorage或Vuex里。
这里的关键细节:密码不能明文比较。BCrypt是单向哈希,每次对同一密码哈希结果不同(因为加了随机盐),所以必须用BCrypt.checkpw()校验,而不是password.equals(user.getPassword())。如果你在数据库里看到密码是$2a$10$xxxx开头的字符串,那就是BCrypt加密后的样子。
接口鉴权:如何让/admin/article/delete只能管理员调用
Spring Security的鉴权不是写在Controller里,而是在配置类SecurityConfig.java中:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启@PreAuthorize注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // 前后端分离,禁用CSRF
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
.and()
.authorizeRequests()
.antMatchers("/admin/login").permitAll() // 登录接口放行
.antMatchers("/admin/**").authenticated() // 其他/admin路径需认证
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
最关键的一步是自定义JwtAuthenticationFilter过滤器。它在每次请求到达Controller前执行:
- 从请求头Authorization: Bearer xxxxx中提取token;
- 调用JwtUtil.parseToken(token)解析出payload(含用户ID);
- 根据用户ID查出用户角色(如ROLE_ADMIN);
- 将角色信息存入Spring Security的SecurityContextHolder;
- 这样后续的@PreAuthorize("hasRole('ADMIN')")注解才能生效。
比如删除文章接口:
@DeleteMapping("/article/{id}")
@PreAuthorize("hasRole('ADMIN')")
public Result deleteArticle(@PathVariable Long id) {
return articleService.removeById(id);
}
@PreAuthorize("hasRole('ADMIN')")的意思是:只有角色为ROLE_ADMIN的用户才能访问。注意:Spring Security默认的角色前缀是ROLE_,所以数据库里存的角色名必须是ADMIN,而不是admin,否则匹配不上。
注意事项:JWT的密钥
"sangeng-secret-key"是硬编码在代码里的,这在生产环境是严重安全隐患。正确做法是放到application.yml的spring.profiles.active对应环境配置里,或者用环境变量注入。新手第一次运行时,如果改了密钥但没同步更新前端的请求头,就会一直报401 Unauthorized——因为前端用旧密钥生成的token,后端用新密钥解析不了。
3.3 基础框架(sangeng-framework):那些让你少写80%样板代码的工具类
sangeng-framework模块的价值,不在于它有多复杂,而在于它把Java开发中最枯燥、最重复的活,变成了开箱即用的工具。我们挑三个最实用的来看。
统一返回Result类:告别if-else嵌套
没有Result类时,Controller可能是这样:
@GetMapping("/article/{id}")
public ResponseEntity<Map<String, Object>> getArticle(@PathVariable Long id) {
try {
Article article = articleService.getById(id);
if (article == null) {
Map<String, Object> error = new HashMap<>();
error.put("code", 404);
error.put("msg", "文章不存在");
return ResponseEntity.status(404).body(error);
}
Map<String, Object> success = new HashMap<>();
success.put("code", 200);
success.put("msg", "成功");
success.put("data", article);
return ResponseEntity.ok(success);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("code", 500);
error.put("msg", "服务器错误");
return ResponseEntity.status(500).body(error);
}
}
15行代码,8行在处理错误和包装。而用了Result后:
@GetMapping("/article/{id}")
public Result<Article> getArticle(@PathVariable Long id) {
Article article = articleService.getById(id);
return article == null ? Result.fail("文章不存在") : Result.success(article);
}
2行搞定。Result.success()和Result.fail()是静态工厂方法,内部自动设置code和msg。这不仅是代码量减少,更是思维模式的转变:你不再纠结“怎么返回”,而是聚焦“业务逻辑是什么”。
全局异常处理器:把500错误变成友好的提示
当代码抛出未捕获的RuntimeException(如空指针、数组越界),Spring Boot默认返回白板错误页。sangeng-framework里的GlobalExceptionHandler把它变成了标准JSON:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
// 记录错误日志
log.error("系统异常", e);
// 返回统一错误响应
return Result.fail("系统繁忙,请稍后再试");
}
@ExceptionHandler(NullPointerException.class)
public Result handleNPE(NullPointerException e) {
log.error("空指针异常", e);
return Result.fail("数据异常,请联系管理员");
}
}
@RestControllerAdvice表示这是一个全局的Controller增强器,它会拦截所有Controller抛出的异常。@ExceptionHandler指定处理哪种异常。这里有两个技巧:
- 先捕获具体的NullPointerException,再捕获泛化的Exception,避免泛化处理器“吃掉”具体异常;
- 在log.error()里打印完整堆栈,但返回给前端的msg是脱敏的,不暴露内部细节(如“java.lang.NullPointerException: Cannot invoke ‘xxx’ because ‘yyy’ is null”这种绝对不能返回)。
JWT工具类:一行代码生成、解析、校验Token
JwtUtil.java封装了JWT的所有操作:
public class JwtUtil {
private static final String SECRET_KEY = "sangeng-secret-key";
// 生成Token
public static String createToken(Map<String, Object> claims, long expireTime) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + expireTime))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
// 解析Token,返回claims(payload)
public static Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
// 校验Token是否过期
public static boolean isTokenExpired(String token) {
try {
Claims claims = parseToken(token);
return claims.getExpiration().before(new Date());
} catch (Exception e) {
return true; // 解析失败也算过期
}
}
}
新手常犯的错误是:在parseToken()里没加try-catch,导致非法token(如被篡改)直接抛出SignatureException,被全局异常处理器捕获,返回“系统繁忙”。正确做法是像上面那样,在工具类内部处理解析失败,返回null或抛出自定义异常,让调用方决定如何处理。
4. 完整实操过程与核心环节实现
4.1 环境准备与项目导入:从零开始的IDEA配置指南
别跳过这一步。很多新手卡在“导入就报错”,不是代码问题,而是环境没配对。以下是我在Windows 11 + IDEA 2023.2 + JDK 17下的实操记录,全程截图式描述。
第一步:确认JDK版本
打开命令行,输入:
java -version
必须输出类似:
java version "17.0.1" 2021-10-19 LTS
Java(TM) SE Runtime Environment (build 17.0.1+12-LTS-39)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.1+12-LTS-39, mixed mode, sharing)
如果显示1.8.0_XXX,说明你用的是JDK 8,必须卸载,安装JDK 17。Spring Boot 2.7+要求最低JDK 17,用低版本会报UnsupportedClassVersionError。
第二步:导入Maven多模块项目
- 打开IDEA,选择
Open(不是New Project),定位到你解压后的项目根目录(即包含pom.xml和sangeng-*文件夹的目录); - IDEA会弹出“Import Project”窗口,勾选
Import project from external model→Maven; - 关键设置:
-Project SDK: 选择你安装的JDK 17;
-Maven home path: 选择你本地的Maven(如C:\apache-maven-3.9.4),不要用IDEA内置的;
-User settings file: 指向你的settings.xml(通常在C:\apache-maven-3.9.4\conf\settings.xml),确保里面有阿里云镜像配置,否则下载依赖巨慢; - 点击
OK,等待IDEA自动下载依赖(首次可能需要10-20分钟)。
第三步:检查Maven结构与模块识别
导入完成后,左侧项目结构应显示:
sangeng-parent (pom)
├── sangeng-framework (jar)
├── sangeng-blog (jar)
└── sangeng-admin (jar)
如果只显示一个sangeng-parent,说明模块没识别出来。此时右键sangeng-parent → Reload project。如果还不行,检查根目录pom.xml的<modules>是否正确:
<modules>
<module>sangeng-framework</module>
<module>sangeng-blog</module>
<module>sangeng-admin</module>
</modules>
注意路径名必须和文件夹名完全一致(大小写敏感),不能是sangeng-framework/(带斜杠)或SangengFramework(大小写不符)。
第四步:配置application.yml数据库连接
三个模块都有自己的src/main/resources/application.yml。你需要修改的是sangeng-blog和sangeng-admin的配置(因为它们要连数据库),sangeng-framework不需要。
打开sangeng-blog/src/main/resources/application.yml,找到spring: datasource部分:
spring:
datasource:
url: jdbc:mysql://localhost:3306/sangeng_blog?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: your_mysql_password
driver-class-name: com.mysql.cj.jdbc.Driver
把your_mysql_password替换成你MySQL的实际密码。如果MySQL没装,现在就去官网下载MySQL Community Server 8.0,安装时记住root密码。重要:URL里的serverTimezone=Asia/Shanghai不能少,否则会报The server time zone value '...' is unrecognized错误。
第五步:初始化数据库表
项目没有提供SQL建表脚本,但MyBatis-Plus支持启动时自动建表。在sangeng-blog的application.yml里添加:
mybatis-plus:
configuration:
auto-mapping-behavior: full
global-config:
db-config:
id-type: assign_id
table-prefix: sg_
然后启动sangeng-blog的SpringBoot应用(右键SangengBlogApplication.java → Run)。首次启动时,控制台会打印大量Creating table sg_article...的日志,说明表已自动创建。如果报错Unknown database 'sangeng_blog',说明数据库sangeng_blog不存在,你需要手动创建:
CREATE DATABASE sangeng_blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
4.2 启动与调试:让三个模块协同工作的真实场景
单模块启动很简单,但要让sangeng-blog和sangeng-admin同时运行,并且互不干扰,需要配置不同的端口和上下文路径。
配置端口与上下文
修改sangeng-blog/src/main/resources/application.yml:
server:
port: 8080
servlet:
context-path: /blog
修改sangeng-admin/src/main/resources/application.yml:
server:
port: 8081
servlet:
context-path: /admin
这样,sangeng-blog的API就在http://localhost:8080/blog/api/**,sangeng-admin的API在http://localhost:8081/admin/api/**,不会端口冲突。
启动顺序与依赖验证
- 先启动
sangeng-framework(右键SangengFrameworkApplication.java→Run),它没有Web服务器,只是加载工具类,瞬间完成; - 再启动
sangeng-blog,等待控制台出现Started SangengBlogApplication in X seconds; - 最后启动
sangeng-admin,同样等待启动完成。
启动后,用浏览器访问:
- http://localhost:8080/blog/api/article/list → 应返回JSON文章列表;
- http://localhost:8081/admin/api/login → POST请求,Body为{"username":"admin","password":"admin123"},应返回带token的JSON。
如果第一个链接404,检查sangeng-blog的Controller类上是否有@RequestMapping("/api"),以及方法上是否有@GetMapping("/article/list)。如果第二个链接401,检查密码是否正确,或者JwtUtil.SECRET_KEY是否被意外修改。
调试登录流程:断点追踪Token生成
这是理解整个认证链路的最佳实践:
1. 在sangeng-admin的LoginController.login()方法第一行打上断点;
2. 用Postman发送登录请求;
3. IDEA会停在断点处,按F8逐行执行;
4. 当执行到JwtUtil.createToken(...)时,进入该方法,观察claims Map里是否包含了正确的用户ID和用户名;
5. 执行完后,查看返回的token字符串,复制它;
6. 用这个token去调用/admin/article/list,验证是否能成功返回文章列表。
通过这个过程,你亲眼看到:密码校验通过 → 用户信息封装进payload → payload用密钥签名 → 生成token → token存入响应头。每一个环节都变得可触摸、可验证。
4.3 功能扩展实战:给博客增加“文章点赞”功能
现在,我们动手改代码,把一个新功能从0做到上线。目标:用户点击文章页的“👍”按钮,该文章点赞数+1。
第一步:数据库加字段
在MySQL里执行:
ALTER TABLE sg_article ADD COLUMN like_count INT DEFAULT 0 COMMENT '点赞数';
然后重启sangeng-blog,MyBatis-Plus会自动识别新字段(因为Article.java里加了private Integer likeCount;和getter/setter)。
第二步:Mapper层增加更新方法
在ArticleMapper.java里添加:
@Update("UPDATE sg_article SET like_count = like_count + 1 WHERE id = #{id}")
int updateLikeCount(@Param("id") Long id);
第三步:Service层封装业务
在ArticleServiceImpl.java里添加方法:
@Transactional // 确保更新操作在事务中
public Result updateLikeCount(Long id) {
int rows = articleMapper.updateLikeCount(id);
return rows > 0 ? Result.success("点赞成功") : Result.fail("点赞失败");
}
第四步:Controller暴露接口
在ArticleController.java里添加:
@PutMapping("/article/{id}/like")
public Result updateLikeCount(@PathVariable Long id) {
return articleService.updateLikeCount(id);
}
第五步:前端调用(伪代码)
在Vue组件里:
handleLike() {
this.$http.put(`/api/article/${this.articleId}/like`)
.then(res => {
this.$message.success(res.data.msg);
this.article.likeCount++; // 本地更新,避免刷新
})
.catch(err => {
this.$message.error(err.response.data.msg);
});
}
第六步:测试与验证
- 启动
sangeng-blog; - 用浏览器访问
http://localhost:8080/blog/api/article/list,记下某篇文章的id(如123)和当前like_count(如0); - 用Postman发送PUT请求到
http://localhost:8080/blog/api/article/123/like; - 再次访问文章列表,确认该文章的like_count变为1。
这个过程看似简单,但涵盖了Java Web开发的全部核心环节:数据库变更、ORM映射、事务控制、RESTful接口设计、前后端联调。你不是在抄代码,而是在构建一个真实的功能闭环。
5. 常见问题与排查技巧实录
5.1 启动报错高频问题速查表
| 报错现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
Failed to configure a DataSource: 'url' attribute is not specified |
application.yml里数据库配置缺失或格式错误 | 检查sangeng-blog/src/main/resources/application.yml,确认spring.datasource.url、username、password三者都存在且缩进正确(YAML对空格敏感) |
补全配置,确保url前有2个空格,username和password与url对齐 |
Caused by: java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver |
MySQL驱动jar包没下载成功 | 查看maven窗口,搜索mysql-connector-java,确认版本是否为8.0.33;检查本地Maven仓库~/.m2/repository/mysql/mysql-connector-java/下是否有对应jar |
删除本地仓库中该目录,重新Reload project;或在pom.xml里显式指定版本 <version>8.0.33</version> |
Invalid bound statement (not found): com.sangeng.blog.mapper.ArticleMapper.selectPublishedListWithXml |
Mapper XML文件没被扫描到 | 检查ArticleMapper.xml是否在src/main/resources/mapper/目录下;检查application.yml里mybatis-plus.mapper-locations是否配置为classpath*:mapper/**/*.xml |
确保XML路径正确,且mybatis-plus.mapper-locations配置无误;重启IDEA |
Whitelabel Error Page(白页) |
Controller方法没加@ResponseBody或类没加@RestController |
检查ArticleController.java类上是否有@RestController,或方法上是否有@ResponseBody |
@RestController = @Controller + @ResponseBody,二者选一即可;推荐用@RestController |
No qualifying bean of type 'com.sangeng.blog.service.ArticleService' |
Service类没被Spring扫描到 | 检查ArticleServiceImpl.java上是否有@Service注解;检查类是否在com.sangeng.blog包及其子包下(Spring Boot默认扫描启动类所在包) |
添加@Service;确保包路径正确;或在启动类上加@ComponentScan("com.sangeng.blog") |
5.2 接口调用失败的深度排查法
当Postman调用/api/article/list返回404,不要急着改代码,按以下顺序排查:
-
确认URL路径是否正确:
http://localhost:8080/blog/api/article/list中的/blog是sangeng-blog的server.servlet.context-path,/api是Controller的@RequestMapping,/article/list是方法的@GetMapping。三者拼接才是完整路径。漏掉任何一个/都会404。 -
检查Controller是否被Spring加载:
启动时控制台搜索Mapped关键字,应看到类似:Mapped "{[/api/article/list], methods=[GET]}" onto public com.sangeng.framework.Result<com.sangeng.framework.PageVo<com.sangeng.blog.vo.ArticleVo>> com.sangeng.blog.controller.ArticleController.getArticleList(com.sangeng.framework.PageVo<com.sangeng.blog.vo.ArticleVo>)
如果没看到,说明Controller类没被扫描到,检查包路径和@RestController注解。 -
验证端口是否被占用:
在命令行执行netstat -ano | findstr :8080(Windows)或lsof -i :8080(Mac/Linux),确认8080端口是sangeng-blog进程在监听,而不是其他程序。 -
关闭防火墙临时测试:
Windows Defender防火墙有时会拦截本地请求。暂时关闭防火墙,再试一次。如果好了,说明是防火墙规则问题,需添加入站规则。
5.3 新手必踩的5个“隐形坑”与避坑指南
坑1:修改了代码但重启后没生效
现象:改了ArticleController的返回消息,重启后还是旧的。
原因:IDEA的Build project没触发,或者Maven没重新编译class文件。
避坑:右键项目 → Maven → Reimport;或按Ctrl+F9(Windows)强制构建;确保Build project automatically已勾选(Settings → Build → Compiler)。
坑2:前端跨域,但后端配置了却无效
现象:浏览器控制台报CORS header ‘Access-Control-Allow-Origin’ missing。
原因:@CrossOrigin注解只对当前Controller有效,而全局跨域配置在sangeng-framework的WebMvcConfig.java里,但sangeng-blog模块没依赖它。
避坑:在sangeng-blog/pom.xml里添加对sangeng-framework的依赖:
<dependency>
<groupId>com.sangeng</groupId>
<artifactId>sangeng-framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
坑3:JWT Token过期后,前端没刷新,一直401
现象:登录后24小时没操作,再点按钮就401。
原因:Token过期是后端校验的,前端不知道,还在用旧token请求。
避坑:在前端axios拦截器里,统一处理401响应:
// request interceptor
service.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// response interceptor
service.interceptors.response.use(response => response, error => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
router.push('/login'); // 跳转登录页
}
return Promise.reject(error);
});
坑4:MyBatis-Plus分页插件不生效,查出所有数据
现象:PageVo传了size=10,但返回了100条数据。
原因:没配置分页插件,或配置在了错误的配置类里。
避坑:在sangeng-framework的MybatisPlusConfig.java里,必须有:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
并且sangeng-blog的pom.xml里必须有mybatis-plus-boot-starter依赖。
坑5:修改了sangeng-framework里的工具类,但sangeng-blog没生效
现象:改了JwtUtil.createToken(),重启sangeng-blog后还是旧逻辑。
原因:Maven模块依赖是编译时的,改了framework的代码,必须先mvn clean install framework模块,再重启blog。
避坑:在IDEA里,右键sangeng-framework → Maven → install,确保控制台显示BUILD SUCCESS,再重启sangeng-blog。
6. 从练手到实战:我的个人经验与进阶建议
这个项目跑通只是起点。我在带新人时发现,真正拉开差距的,不是谁能更快写出CRUD,而是谁能在跑通后,主动去“破坏”它、理解它、重构它。分享几个我亲身验证过的进阶路径。
第一阶段:读懂每一行日志
启动sangeng-blog后,盯着控制台日志看10分钟。重点关注:
- Starting SangengBlogApplication on XXX with PID XXX:这是Spring Boot容器启动的起点;
- Mapped "{[/api/article/list], methods=[GET]}":这是Spring MVC建立的路由映射;
- Creating shared instance of singleton bean 'articleService':这是Spring IOC容器创建Bean的过程;
- JDBC Connection [HikariProxyConnection@XXX] will not be managed by Spring:这是数据库连接池获取连接的日志。
当你能从日志里读出“Spring Boot正在做什么”,你就超越了90%的新手。日志不是噪音,是系统运行的脉搏。
第二阶段:给项目加监控
在sangeng-blog/pom.xml里加入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
然后在application.yml里配置:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,loggers
endpoint:
health:
show-details: always
启动后访问http://localhost:8080/blog/actuator/health,你会看到一个JSON,告诉你数据库是否连通、磁盘空间是否充足。再访问/actuator/metrics,能看到JVM内存、HTTP请求数等指标。这才是生产级应用该有的样子——可观测性,是运维的基石。
第三阶段:用Docker容器化部署
别满足于本地运行。写一个Dockerfile放在项目根目录:
FROM openjdk:17-jdk-slim
VOLUME /tmp
ARG JAR_FILE=target/sangeng-blog-1.0-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
再写一个docker-compose.yml:
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: sangeng_blog
ports:
- "3306:3306"
blog:
build: .
depends_on:
- mysql
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/sangeng_blog
ports:
- "8080:8080"
运行docker-compose up -d,整个博客系统就跑在容器里了。你不再依赖本地MySQL,也不用担心端口冲突。容器化,是现代Java开发的成人礼。
最后想说,这个sangeng-blog项目最珍贵的地方,不是它实现了多少功能,而是它用最朴实的代码,展示了Java生态里最核心的协作逻辑:Spring Boot负责“粘合”,MyBatis-Plus负责“数据搬运”,Spring Security负责“守门”,JWT负责“身份凭证”,而你,是那个站在所有轮子之上,指挥它们协同工作的工程师。当你第一次成功给文章点赞,看着数据库里like_count真的+1了,那一刻的成就感,就是编程最本真的快乐。别急着学新框架,先把这三个模块的每一行代码,都变成你肌肉记忆的一部分。
简介:这个SpringBoot博客项目专为刚学完Java基础和SpringBoot入门的学习者设计,包含三个核心模块:sangeng-blog(用户浏览的博客前台)、sangeng-admin(管理员使用的后台系统)、sangeng-framework(可复用的基础功能模块)。项目采用前后端分离结构,前端页面完整,后端接口清晰,支持文章发布、分类管理、评论互动、用户登录等常见博客功能。所有代码基于标准Maven结构组织,pom.xml依赖配置齐全,.gitignore文件已预置,README.md提供基础运行指引,可直接导入IDEA或Eclipse一键启动。目录结构体现典型的企业级分层思想——controller层处理请求、service层封装逻辑、mapper层对接数据库、entity和dto负责数据传输,方便理解模块化开发流程。配套代码注释较充分,适合边跑边读、边改边学,是巩固SpringBoot核心组件(如Spring MVC、MyBatis Plus、Spring Security、JWT)实战能力的实用练手素材。
更多推荐




所有评论(0)