本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个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-starterlombok。它的存在意义在于:当sangeng-blog和sangeng-admin都需要发邮件、都需要操作Redis、都需要统一返回格式时,你不用在两个模块里各写一遍,而是让它们都依赖sangeng-framework。这就是“复用”的真实模样——不是抽象成高大上的设计模式,而是把重复代码抽成一个jar包。

提示:新手常犯的错误是试图在sangeng-framework里写业务Service。记住:framework模块里永远不该出现ArticleService、CategoryService这类业务类。它的边界很清晰——只做“所有业务都需要的、和具体业务无关的事”。

2.2 为什么坚持前后端分离?一次HTTP请求的完整旅程

很多人觉得“前后端分离”就是前端用Vue、后端用SpringBoot,然后用axios调接口。这没错,但没抓住本质。本质是关注点分离:前端工程师只关心“怎么把数据漂亮地展示出来”,后端工程师只关心“怎么把数据准确、安全、高效地查出来”。

我们以用户访问一篇博客文章为例,走一遍完整的链路:

  1. 用户在浏览器地址栏输入 https://blog.example.com/article/123
  2. 前端Vue路由捕获这个URL,触发ArticleDetail.vue组件的mounted()钩子;
  3. 组件内执行 this.$http.get('/api/article/123'),发起一个GET请求到后端;
  4. 请求到达sangeng-blog模块的ArticleController,其方法签名是:
    java @GetMapping("/article/{id}") public Result<ArticleVo> getArticleById(@PathVariable Long id) { ... }
  5. Controller调用articleService.getById(id),Service层查数据库,Mapper层执行SQL;
  6. 数据查出来后,Controller不返回HTML,而是封装成Result<ArticleVo>对象,其中ArticleVo是专门给前端用的视图对象(可能包含文章内容、作者昵称、分类名称、评论数等,字段比数据库实体Article更丰富);
  7. SpringBoot的@RestController自动将Result对象序列化为JSON,通过HTTP响应体返回给前端;
  8. 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.javasangeng-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生成

  1. 前端提交用户名密码到/admin/login接口;
  2. LoginController.java接收请求,调用loginService.login(username, password)
  3. 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
  4. 将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多模块项目

  1. 打开IDEA,选择Open(不是New Project),定位到你解压后的项目根目录(即包含pom.xml和sangeng-*文件夹的目录);
  2. IDEA会弹出“Import Project”窗口,勾选Import project from external modelMaven
  3. 关键设置:
    - 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),确保里面有阿里云镜像配置,否则下载依赖巨慢;
  4. 点击OK,等待IDEA自动下载依赖(首次可能需要10-20分钟)。

第三步:检查Maven结构与模块识别

导入完成后,左侧项目结构应显示:

sangeng-parent (pom)
├── sangeng-framework (jar)
├── sangeng-blog (jar)
└── sangeng-admin (jar)

如果只显示一个sangeng-parent,说明模块没识别出来。此时右键sangeng-parentReload 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-blogsangeng-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-blogapplication.yml里添加:

mybatis-plus:
  configuration:
    auto-mapping-behavior: full
  global-config:
    db-config:
      id-type: assign_id
      table-prefix: sg_

然后启动sangeng-blog的SpringBoot应用(右键SangengBlogApplication.javaRun)。首次启动时,控制台会打印大量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/**,不会端口冲突。

启动顺序与依赖验证

  1. 先启动sangeng-framework(右键SangengFrameworkApplication.javaRun),它没有Web服务器,只是加载工具类,瞬间完成;
  2. 再启动sangeng-blog,等待控制台出现Started SangengBlogApplication in X seconds
  3. 最后启动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-adminLoginController.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);
    });
}

第六步:测试与验证

  1. 启动sangeng-blog
  2. 用浏览器访问http://localhost:8080/blog/api/article/list,记下某篇文章的id(如123)和当前like_count(如0);
  3. 用Postman发送PUT请求到http://localhost:8080/blog/api/article/123/like
  4. 再次访问文章列表,确认该文章的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.urlusernamepassword三者都存在且缩进正确(YAML对空格敏感) 补全配置,确保url前有2个空格,usernamepasswordurl对齐
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.ymlmybatis-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,不要急着改代码,按以下顺序排查:

  1. 确认URL路径是否正确

    http://localhost:8080/blog/api/article/list 中的 /blogsangeng-blogserver.servlet.context-path/api是Controller的@RequestMapping/article/list是方法的@GetMapping。三者拼接才是完整路径。漏掉任何一个/都会404。

  2. 检查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注解。

  3. 验证端口是否被占用

    在命令行执行netstat -ano | findstr :8080(Windows)或lsof -i :8080(Mac/Linux),确认8080端口是sangeng-blog进程在监听,而不是其他程序。

  4. 关闭防火墙临时测试

    Windows Defender防火墙有时会拦截本地请求。暂时关闭防火墙,再试一次。如果好了,说明是防火墙规则问题,需添加入站规则。

5.3 新手必踩的5个“隐形坑”与避坑指南

坑1:修改了代码但重启后没生效

现象:改了ArticleController的返回消息,重启后还是旧的。

原因:IDEA的Build project没触发,或者Maven没重新编译class文件。

避坑:右键项目 → MavenReimport;或按Ctrl+F9(Windows)强制构建;确保Build project automatically已勾选(Settings → Build → Compiler)。

坑2:前端跨域,但后端配置了却无效

现象:浏览器控制台报CORS header ‘Access-Control-Allow-Origin’ missing

原因:@CrossOrigin注解只对当前Controller有效,而全局跨域配置在sangeng-frameworkWebMvcConfig.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-frameworkMybatisPlusConfig.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-frameworkMaveninstall,确保控制台显示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了,那一刻的成就感,就是编程最本真的快乐。别急着学新框架,先把这三个模块的每一行代码,都变成你肌肉记忆的一部分。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个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)实战能力的实用练手素材。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐