news 2026/9/4 8:03:44

基于SSM框架构建个人博客系统:从数据库设计到安全部署全流程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于SSM框架构建个人博客系统:从数据库设计到安全部署全流程实践

简介:本资源是一套面向JavaWeb初学者与课程设计学生的个人博客系统全栈开发学习包,聚焦SSM框架(Spring+SpringMVC+MyBatis)实战,解决从需求分析、架构设计到编码部署的完整开发闭环问题。压缩包共19个文件,含3个PDF(含论文、开发手册、词汇表)、1个PPTX(系统设计讲解)、1个DOC(详细文档)、1个SQL(建库建表脚本)、3个PNG(项目运行截图)、2个ZIP(含源码的双版本工程)、4个URL(配套视频课程入口)及1个TXT说明文件,总大小44.52MB,结构清晰、资料互补性强。已有756人下载学习,适合高校课程设计、毕业设计或自学进阶者。学习者可直接获取可运行的SSM博客源码、数据库脚本、完整论文与答辩PPT、分模块讲解视频链接,以及涵盖MVC分层、Session会话管理、JDBC操作、防XSS/SQL注入等关键实践要点的设计实现解析,大幅降低开发门槛并提升工程规范意识。

1. 项目概述:从零到一,构建你的数字自留地

“个人博客系统”,这七个字对于很多开发者来说,就像是一个技术上的“成人礼”。它不像电商、OA那样庞大复杂,也不像“Hello World”那样过于简单。它恰好卡在一个完美的位置:功能完整,覆盖了Web开发的绝大多数核心模块;规模适中,一个开发者足以驾驭;同时又极具个人价值,是你技术理念和思考沉淀的最佳载体。这次,我们就来聊聊如何用经典的SSM框架(Spring + Spring MVC + MyBatis)亲手搭建一个属于你自己的、五脏俱全的博客系统。这不仅仅是一个项目实现,更是一次对Java Web开发核心技术的系统性梳理与实践。

为什么是SSM?在Spring Boot大行其道的今天,回过头来搞SSM是不是有点“复古”?恰恰相反。SSM是理解Spring生态的基石。Spring Boot帮你做了很多“约定大于配置”的事情,但如果你不清楚这些约定背后的“配置”是什么,遇到复杂问题就容易抓瞎。通过亲手搭建SSM项目,你会清晰地知道一个请求是如何从浏览器出发,经过DispatcherServlet,找到对应的Controller,调用Service处理业务逻辑,再通过MyBatis与数据库交互,最后将数据渲染到视图返回给用户的。这个完整的流程,是构建你后端知识体系不可或缺的一环。

这个博客系统将包含哪些核心功能?简单来说,就是一个内容创作者最需要的工具集:文章的管理(增删改查)、分类与标签体系、评论互动,以及最基础的权限控制(至少区分管理员和访客)。实现这些功能的过程,你会遇到并解决Web开发中的典型问题:如何设计优雅的RESTful API?如何实现前后端数据的高效交互?如何管理数据库事务?如何防止XSS或SQL注入?这些问题的答案,都将体现在你的代码里。

2. 技术选型与架构设计解析

2.1 为什么选择SSM框架组合

SSM是Spring、Spring MVC和MyBatis三个框架的集成,在Java企业级开发中曾长期占据统治地位,其分层架构思想至今仍是优秀设计的典范。

Spring: 作为核心的IoC(控制反转)容器。它的价值在于“管理”。在没有Spring的时代,对象之间的依赖关系需要我们在代码里手动new出来,耦合度极高。Spring通过配置文件或注解,接管了对象的创建、组装和管理。在这个博客系统里,我们的Controller、Service、Dao(Mapper)这些组件,都不需要自己实例化,而是由Spring容器统一生成并注入所需的依赖。比如,ArticleServiceImpl需要用到ArticleMapper,我们只需要在ArticleServiceImpl中声明@Autowired,Spring就会自动把ArticleMapper的实现类“装配”进来。这带来的最大好处是便于测试和配置切换,因为依赖关系是外部注入的,而非内部硬编码。

Spring MVC: 处理Web请求的模型-视图-控制器框架。它清晰地划分了职责。当用户访问/article/1这个URL时,Spring MVC的DispatcherServlet(前端控制器)会拦截这个请求,根据配置的映射关系,找到对应的ArticleController中的某个方法(比如getArticleById)来处理。Controller方法负责协调:调用ArticleService获取文章数据,然后将数据放入Model中,最后返回一个视图名(如"articleDetail")或者直接返回JSON/XML数据(RESTful API)。这种模式让控制逻辑、业务逻辑和视图渲染分离,代码结构非常清晰。

MyBatis: 一个优秀的持久层框架。它介于全自动的Hibernate和全手动的JDBC之间,提供了极大的灵活性。我们通过编写简洁的SQL映射文件(Mapper.xml)或使用注解,将Java方法(Mapper接口中的方法)与SQL语句绑定起来。相比于Hibernate的HQL,MyBatis允许你编写和优化原生的SQL,这对于需要进行复杂查询或性能调优的场景(比如博客的文章列表分页+多条件筛选)非常有利。同时,它又避免了JDBC中大量的样板代码(如手动设置参数、遍历ResultSet),通过动态SQL、结果集自动映射等特性,极大地提升了开发效率。

提示:对于新手,我建议从XML配置SQL开始,而不是纯注解。XML文件能更直观地展示复杂的动态SQL(如<if>,<choose>标签),也便于后期统一管理和优化SQL语句。

2.2 系统分层架构设计

一个结构清晰的系统是长期可维护的基础。我们采用经典的四层架构:

  1. 表现层(Presentation Layer):由Spring MVC的Controller承担。它非常“薄”,只负责接收HTTP请求、解析参数、调用业务层、处理异常、返回响应(视图或JSON)。它不应该包含任何业务逻辑。例如,ArticleController里的一个发布文章方法,可能只有几行代码:校验参数、调用articleService.publish(article)、返回成功或失败信息。
  2. 业务逻辑层(Service Layer):这是系统的“大脑”。所有的业务规则、流程控制、事务管理都在这里。例如,ArticleService中“发布文章”这个方法,内部可能包含:检查文章标题是否重复、将文章状态设为“已发布”、更新文章发布时间、同步更新分类/标签的文章计数、发送通知(如果需要)等一系列操作。这一层的方法通常使用@Transactional注解,保证一个业务方法内的数据库操作要么全部成功,要么全部回滚。
  3. 持久层(Persistence Layer):由MyBatis的Mapper接口和对应的XML文件组成。它的职责非常单一:与数据库对话,执行CRUD操作。它不应该出现任何业务判断。ArticleMapper只提供insert,selectById,update,delete等方法,至于什么时候调用、调用后干什么,是Service层的事。
  4. 领域模型层(Domain Model Layer):即实体类(Entity),如ArticleCategoryComment。它们是数据的载体,在各层之间传递。设计良好的实体类应该是一个“贫血模型”(主要包含属性和getter/setter),业务行为尽量放在Service中。

此外,我们还需要一些支撑组件

  • 工具层(Utils):放置字符串处理、日期格式化、加密解密、验证码生成等通用工具类。
  • 配置层(Config):集中管理Spring、Spring MVC、MyBatis、数据库连接池(如Druid)、事务等的配置。在SSM中,这些通常通过applicationContext.xmlspring-mvc.xmlmybatis-config.xml等XML文件或Java Config类来定义。

这种分层架构像一套流水线,请求数据从Controller流入,经过Service加工,通过Mapper存入数据库,或者反向流动。每一层各司其职,边界清晰,使得代码易于理解、测试和维护。

3. 数据库设计与核心表结构剖析

数据库设计是项目的基石,设计得好,后期开发顺风顺水;设计得差,到处是坑。对于博客系统,我们至少需要以下几张核心表。

3.1 文章表(t_article):内容的核心载体

这是最重要的表。字段设计需要兼顾功能、性能和扩展性。

CREATE TABLE `t_article` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `title` varchar(200) NOT NULL COMMENT '文章标题', `summary` varchar(500) DEFAULT NULL COMMENT '文章摘要', `cover_image` varchar(500) DEFAULT NULL COMMENT '封面图片URL', `content` longtext NOT NULL COMMENT '文章内容(HTML格式)', `content_md` longtext COMMENT '文章内容(Markdown源格式)', `category_id` int(11) DEFAULT NULL COMMENT '分类ID', `is_top` tinyint(1) DEFAULT '0' COMMENT '是否置顶(0否,1是)', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态(0草稿,1已发布,2私密)', `view_count` int(11) DEFAULT '0' COMMENT '浏览量', `like_count` int(11) DEFAULT '0' COMMENT '点赞数', `comment_count` int(11) DEFAULT '0' COMMENT '评论数', `allow_comment` tinyint(1) DEFAULT '1' COMMENT '是否允许评论', `password` varchar(100) DEFAULT NULL COMMENT '访问密码(用于私密文章)', `author_id` bigint(20) NOT NULL COMMENT '作者ID', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `publish_time` datetime DEFAULT NULL COMMENT '发布时间', PRIMARY KEY (`id`), KEY `idx_category_id` (`category_id`), KEY `idx_status_publish_time` (`status`,`publish_time`), KEY `idx_author_id` (`author_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表';

设计要点与避坑指南:

  1. 内容分离存储:我强烈建议同时存储content(HTML)和content_md(Markdown)。后台编辑使用Markdown,存储源格式,便于再次编辑。前台展示时,直接渲染已转换好的HTML,提升性能。避免每次请求都进行Markdown解析。
  2. 计数字段反范式设计:view_countcomment_count等字段是典型的“反范式”设计。理论上,浏览量可以通过计数表SUM出来,评论数可以通过关联t_commentCOUNT出来。但在高并发读取的场景下,这种关联查询是性能杀手。直接在文章表冗余这些计数,用空间换时间,是博客这类读多写少系统的常见优化手段。更新这些计数时,使用乐观锁或update t_article set view_count = view_count + 1 where id = ?这样的原子操作,避免并发问题。
  3. 索引策略:idx_status_publish_time这个联合索引至关重要。前台列表页最常见的查询就是“查询状态为已发布(status=1)的文章,按发布时间倒序排列”。这个索引能完美覆盖这个查询,避免全表扫描和文件排序(filesort)。idx_category_id用于按分类筛选,idx_author_id用于后台管理按作者查询。
  4. datetime与时间戳:使用datetime类型更直观。CURRENT_TIMESTAMPON UPDATE CURRENT_TIMESTAMP可以自动管理创建和更新时间,非常方便。

3.2 分类表(t_category)与标签表(t_tag

分类是树状或平行的,标签是平面的。这里我们设计为平行分类。

CREATE TABLE `t_category` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '分类名称', `description` varchar(200) DEFAULT NULL COMMENT '分类描述', `sort` int(11) DEFAULT '0' COMMENT '排序值(越大越靠前)', `article_count` int(11) DEFAULT '0' COMMENT '该分类下文章数量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章分类表';

标签表结构类似,但多了一个“文章-标签”关系表,因为是多对多关系。

CREATE TABLE `t_tag` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '标签名称', `color` varchar(20) DEFAULT '#666' COMMENT '标签颜色(前端显示用)', `article_count` int(11) DEFAULT '0', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表'; CREATE TABLE `t_article_tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `article_id` bigint(20) NOT NULL, `tag_id` int(11) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_article_tag` (`article_id`,`tag_id`), -- 防止重复关联 KEY `idx_tag_id` (`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章-标签关联表';

设计要点:同样,article_count是反范式冗余。更新逻辑是:当文章发布/删除时,同步更新其所属分类和所有关联标签的article_count。这个操作要在同一个事务中完成,保证数据一致性。

3.3 评论表(t_comment):实现互动

评论设计可以很简单,也可以很复杂(支持楼中楼)。我们先实现一个平铺的评论系统。

CREATE TABLE `t_comment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `article_id` bigint(20) NOT NULL COMMENT '所属文章ID', `parent_id` bigint(20) DEFAULT '0' COMMENT '父评论ID(0表示顶级评论)', `reply_to_id` bigint(20) DEFAULT NULL COMMENT '回复的目标评论ID', `user_id` bigint(20) DEFAULT NULL COMMENT '评论用户ID(未登录则为空)', `nickname` varchar(100) NOT NULL COMMENT '昵称', `email` varchar(100) DEFAULT NULL COMMENT '邮箱(用于接收回复通知)', `website` varchar(200) DEFAULT NULL COMMENT '个人网站', `content` text NOT NULL COMMENT '评论内容', `user_agent` varchar(500) DEFAULT NULL COMMENT '用户浏览器标识', `ip_address` varchar(100) DEFAULT NULL COMMENT '评论者IP', `is_admin` tinyint(1) DEFAULT '0' COMMENT '是否为博主评论', `status` tinyint(4) DEFAULT '1' COMMENT '状态(0待审核,1已发布,2垃圾评论)', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_article_id` (`article_id`), KEY `idx_parent_id` (`parent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';

设计要点:即使未来要支持楼中楼,parent_idreply_to_id的字段设计也预留了空间。user_agentip_address对于反垃圾和运营分析很有用。status字段实现了评论审核机制。

4. 后端核心功能实现详解

4.1 集成SSM框架与基础配置

首先,创建一个Maven项目,在pom.xml中引入核心依赖:Spring Context、Spring MVC、MyBatis、MyBatis-Spring整合包、数据库驱动(如MySQL)、连接池(如Alibaba Druid,它提供监控功能)、Jackson(处理JSON)、JSTL(JSP标签库)等。

关键的配置文件有三个:

  1. web.xml: 配置Spring的监听器(ContextLoaderListener)来加载根应用上下文,配置DispatcherServlet来加载Spring MVC上下文,并设置字符编码过滤器。
  2. applicationContext.xml(根配置): 这里配置非Web相关的Bean。核心包括:
    • 数据源DataSource(DruidDataSource)。
    • 事务管理器PlatformTransactionManager
    • SqlSessionFactoryBean,指定MyBatis配置文件和Mapper扫描路径。
    • 使用<context:component-scan>扫描Service和Dao层的组件(排除Controller层)。
  3. spring-mvc.xml(Web配置): 配置Web相关Bean。
    • 启用注解驱动<mvc:annotation-driven/>,这会自动注册JSON转换器、验证器等。
    • 配置静态资源处理<mvc:resources .../>,避免静态文件被DispatcherServlet拦截。
    • 配置视图解析器InternalResourceViewResolver,用于JSP页面。
    • 使用<context:component-scan>扫描Controller层组件。

注意:很多初学者在这里会踩坑,Spring和Spring MVC的组件扫描范围一定要区分开。通常,Spring(applicationContext.xml)扫描service,dao,component包;Spring MVC(spring-mvc.xml)扫描controller包。如果重复扫描,会导致Bean被创建两次,引发意外行为。

4.2 文章管理模块的Service层与事务控制

业务逻辑是核心。以发布文章为例,我们来看ArticleServiceImpl

@Service public class ArticleServiceImpl implements ArticleService { @Autowired private ArticleMapper articleMapper; @Autowired private CategoryMapper categoryMapper; @Autowired private TagMapper tagMapper; @Autowired private ArticleTagMapper articleTagMapper; @Override @Transactional(rollbackFor = Exception.class) // 声明式事务管理 public Result publishArticle(Article article, List<Integer> tagIds) { // 1. 参数校验 if (StringUtils.isBlank(article.getTitle())) { return Result.fail("文章标题不能为空"); } // 2. 设置文章基础信息 article.setStatus(ArticleStatus.PUBLISHED.getCode()); article.setPublishTime(new Date()); // 3. 插入文章主体 articleMapper.insertSelective(article); // 使用Selective,避免更新null字段 Long articleId = article.getId(); // 获取自增ID // 4. 更新分类文章数 if (article.getCategoryId() != null) { categoryMapper.increaseArticleCount(article.getCategoryId()); } // 5. 处理标签关联 if (CollectionUtils.isNotEmpty(tagIds)) { for (Integer tagId : tagIds) { ArticleTagRelation relation = new ArticleTagRelation(); relation.setArticleId(articleId); relation.setTagId(tagId); articleTagMapper.insert(relation); // 更新标签文章数 tagMapper.increaseArticleCount(tagId); } } // 6. 其他逻辑,如清理缓存、发送通知等... return Result.success("发布成功", articleId); } }

关键点解析:

  • @Transactional:这是Spring声明式事务管理的核心。它保证了从insertSelective到所有increaseArticleCount操作,要么全部成功,要么全部回滚。rollbackFor = Exception.class确保了即使发生非运行时异常(如IOException)也会回滚。
  • insertSelective:这是MyBatis Generator生成的常用方法。它与insert的区别在于,只会插入属性值不为null的字段,这在部分更新或字段有默认值时非常有用。
  • 业务逻辑集中:发布一篇文章,不仅仅是insert一条记录。它包含了状态变更、计数更新、关系维护等一系列操作。Service层将这些操作原子性地组织在一起。
  • 参数校验:虽然前端会校验,但后端校验是必须的防线。更复杂的校验可以使用JSR-303注解(如@NotBlank)配合@Valid注解。

4.3 MyBatis动态SQL与复杂查询实践

博客首页的文章列表通常需要分页,并且支持按分类、标签、关键词等多条件查询。MyBatis的动态SQL功能在这里大放异彩。

首先,在ArticleMapper.xml中定义查询语句:

<!-- ArticleQueryParam 是一个自定义的查询参数封装类 --> <select id="selectArticlesByPage" resultMap="BaseResultMap" parameterType="com.yourblog.model.param.ArticleQueryParam"> SELECT a.*, c.name as category_name FROM t_article a LEFT JOIN t_category c ON a.category_id = c.id WHERE a.status = 1 <!-- 只查询已发布的文章 --> <if test="categoryId != null"> AND a.category_id = #{categoryId} </if> <if test="tagId != null"> AND EXISTS ( SELECT 1 FROM t_article_tag at WHERE at.article_id = a.id AND at.tag_id = #{tagId} ) </if> <if test="keyword != null and keyword != ''"> AND (a.title LIKE CONCAT('%', #{keyword}, '%') OR a.summary LIKE CONCAT('%', #{keyword}, '%')) </if> ORDER BY <choose> <when test="orderBy == 'view'">a.view_count DESC, a.publish_time DESC</when> <when test="orderBy == 'like'">a.like_count DESC, a.publish_time DESC</when> <otherwise>a.is_top DESC, a.publish_time DESC</otherwise><!-- 默认:置顶优先,按发布时间倒序 --> </choose> </select>

对应的Mapper接口:

public interface ArticleMapper { List<Article> selectArticlesByPage(ArticleQueryParam param); // 其他方法... }

在Service中调用,并结合分页插件(如PageHelper):

@Override public PageResult<ArticleVO> getArticleList(ArticleQueryParam param, Integer pageNum, Integer pageSize) { // 使用PageHelper.startPage方法后,紧跟着的第一个MyBatis查询方法会被分页 PageHelper.startPage(pageNum, pageSize); List<Article> articles = articleMapper.selectArticlesByPage(param); PageInfo<Article> pageInfo = new PageInfo<>(articles); // 将Article实体转换为前端需要的VO(View Object),例如补充分类名、标签列表等 List<ArticleVO> articleVOs = convertToVOList(articles); return new PageResult<>(pageInfo.getTotal(), articleVOs); }

避坑指南:

  1. PageHelper使用警告:PageHelper.startPage(pageNum, pageSize)必须紧贴在要分页的Mapper查询方法调用之前,中间不能有其它数据库查询操作,否则分页会失效或作用于错误的语句。
  2. SQL性能:上述查询中的EXISTS子查询和LIKE '%keyword%'在数据量大时性能很差。对于标签筛选,更好的做法是先通过关联表t_article_tag查询出文章ID集合,再用IN查询。对于全文搜索,应考虑引入Elasticsearch或数据库自带的全文索引(如MySQL的FULLTEXT INDEX)。
  3. 结果映射:当查询涉及多表连接(如这里左连了分类表),MyBatis的resultMap可以非常精细地定义如何将结果集映射到Java对象及其嵌套对象上,这是MyBatis比JPA的@OneToMany等注解更灵活的地方。

5. 前端展示与用户交互实现

5.1 后端Controller设计与RESTful API

前后端分离是主流,即使使用JSP渲染,API的设计也应清晰。我们为文章模块设计一组RESTful风格的API:

  • GET /api/articles- 获取文章列表(分页+筛选)
  • GET /api/articles/{id}- 获取单篇文章详情
  • GET /api/articles/{id}/comments- 获取文章评论
  • POST /api/articles/{id}/comments- 发表评论
  • POST /api/articles/{id}/like- 点赞文章

对应的Controller:

@RestController // @Controller + @ResponseBody,直接返回JSON @RequestMapping("/api/articles") public class ArticleApiController { @Autowired private ArticleService articleService; @Autowired private CommentService commentService; @GetMapping public Result<PageResult<ArticleVO>> listArticles( @RequestParam(required = false) Integer categoryId, @RequestParam(required = false) String keyword, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { ArticleQueryParam param = new ArticleQueryParam(); param.setCategoryId(categoryId); param.setKeyword(keyword); PageResult<ArticleVO> pageResult = articleService.getArticleList(param, page, size); return Result.success(pageResult); } @GetMapping("/{id}") public Result<ArticleDetailVO> getArticle(@PathVariable Long id) { // 增加浏览量(注意防刷) articleService.increaseViewCount(id); ArticleDetailVO detail = articleService.getArticleDetailById(id); return Result.success(detail); } @PostMapping("/{id}/comments") public Result addComment(@PathVariable Long id, @Valid @RequestBody Comment comment, HttpServletRequest request) { comment.setArticleId(id); // 从请求中获取IP等信息 comment.setIpAddress(IpUtil.getClientIp(request)); comment.setUserAgent(request.getHeader("User-Agent")); commentService.addComment(comment); return Result.success("评论发表成功,等待审核"); } }

关键点:

  • @RestController:自动将返回对象序列化为JSON。
  • @PathVariable@RequestParam:用于获取URL路径参数和查询字符串参数。
  • @Valid:触发对Comment对象字段的JSR-303校验(如@NotBlank)。
  • 统一响应体:所有API返回Result<T>对象,包含codemsgdata字段,便于前端统一处理。

5.2 前端页面渲染与交互(以JSP为例)

虽然现在更流行前后端分离(Vue/React + API),但理解服务端渲染仍有价值。在spring-mvc.xml中配置的视图解析器会将Controller返回的视图名解析为JSP文件。

一个简单的文章列表页Controller:

@Controller @RequestMapping("/") public class IndexController { @Autowired private ArticleService articleService; @Autowired private CategoryService categoryService; @GetMapping public String index(@RequestParam(defaultValue = "1") Integer page, Model model) { PageResult<ArticleVO> pageResult = articleService.getArticleList(new ArticleQueryParam(), page, 10); List<Category> categories = categoryService.getAllCategories(); model.addAttribute("articlePage", pageResult); model.addAttribute("categories", categories); return "index"; // 对应 /WEB-INF/views/index.jsp } }

index.jsp中,使用JSTL和EL表达式渲染数据:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <div class="category-list"> <c:forEach items="${categories}" var="cat"> <a href="/?categoryId=${cat.id}">${cat.name}</a> </c:forEach> </div> <div class="article-list"> <c:forEach items="${articlePage.list}" var="article"> <article> <h2><a href="/article/${article.id}">${article.title}</a></h2> <p class="summary">${article.summary}</p> <div class="meta"> <span>${article.publishTime}</span> | <span>分类:${article.categoryName}</span> | <span>阅读(${article.viewCount})</span> </div> </article> </c:forEach> </div> <!-- 分页组件 --> <div class="pagination"> <c:if test="${articlePage.hasPreviousPage}"> <a href="/?page=${articlePage.prePage}">上一页</a> </c:if> <c:forEach begin="1" end="${articlePage.pages}" var="p"> <c:choose> <c:when test="${p == articlePage.pageNum}"> <strong>${p}</strong> </c:when> <c:otherwise> <a href="/?page=${p}">${p}</a> </c:otherwise> </c:choose> </c:forEach> <c:if test="${articlePage.hasNextPage}"> <a href="/?page=${articlePage.nextPage}">下一页</a> </c:if> </div>

实操心得:在JSP中,逻辑应尽量简单,只做数据展示。复杂的计算或数据准备应在Controller或Service中完成。保持JSP的整洁,有利于后期维护和向模板引擎(如Thymeleaf)或前后端分离架构迁移。

6. 部署、优化与安全加固

6.1 项目打包与部署

开发完成后,使用Maven打包:mvn clean package。这会生成一个WAR包(如my-blog.war)。将其部署到Tomcat的webapps目录下,启动Tomcat即可。

更现代的部署方式是使用Spring Boot内嵌Tomcat,将项目打包成可执行的JAR文件。这需要将项目重构为Spring Boot项目,但部署会变得极其简单:java -jar my-blog.jar。对于SSM项目,可以通过添加Spring Boot Starter依赖并创建一个主类来快速迁移。

6.2 性能优化要点

  1. 数据库连接池:务必使用Druid或HikariCP等高性能连接池,并在配置中设置合理的初始大小、最大连接数、超时时间。
  2. MyBatis二级缓存:对于更新频率极低的数据(如分类、标签列表),可以开启MyBatis的二级缓存。但要注意,在分布式环境下,本地缓存会导致数据不一致,此时应使用Redis等集中式缓存。
  3. 静态资源缓存:在Nginx或Tomcat中为CSS、JS、图片等静态资源设置长时间的缓存头(如Cache-Control: max-age=31536000),利用浏览器缓存大幅减少请求。
  4. CDN加速:将静态资源上传至CDN,加快不同地区用户的访问速度。
  5. SQL优化:使用EXPLAIN分析慢查询,为高频查询条件建立合适的索引。避免SELECT *,只查询需要的字段。

6.3 安全防护措施

  1. SQL注入:MyBatis使用#{}预编译占位符,默认即可防止SQL注入。绝对禁止在MyBatis中通过字符串拼接(${})来构造SQL语句,除非你非常清楚自己在做什么并且做了严格的过滤。
  2. XSS攻击:用户提交的内容(评论、文章内容)在展示前必须进行转义。可以在前端渲染时处理,也可以在后端存储前处理。JSTL的<c:out value="${content}">默认会进行HTML转义。对于富文本内容(如文章详情),需要采用白名单过滤的机制(如使用Jsoup库)。
  3. CSRF攻击:在表单提交中(如发表评论),应加入CSRF Token进行验证。Spring Security提供了开箱即用的支持。
  4. 敏感信息过滤:在日志中,避免直接打印用户的密码、身份证号等完整信息。在applicationContext.xml中配置数据库连接池时,密码应使用加密后的形式或从环境变量读取。
  5. 接口限流与防刷:对于点赞、获取验证码等接口,应基于IP或用户进行限流,防止恶意刷接口。可以使用Guava的RateLimiter或Redis实现简单的计数器算法。

7. 常见问题排查与调试技巧

在开发SSM博客系统的过程中,你几乎一定会遇到下面这些问题。这里我把自己踩过的坑和解决方法记录下来。

问题现象可能原因排查步骤与解决方案
启动Tomcat时报ClassNotFoundExceptionNoClassDefFoundError1. 依赖未正确导入。
2. 依赖冲突(多个版本)。
3. WAR包中缺少JAR。
1. 运行mvn dependency:tree查看依赖树,检查目标类所在的JAR是否存在。
2. 使用<exclusions>排除冲突的传递性依赖。
3. 检查Tomcat的lib目录或项目的WEB-INF/lib下是否有对应JAR。
页面访问404,但控制台无错误1.@RequestMapping路径写错。
2. Controller未被Spring扫描到。
3. 静态资源路径被DispatcherServlet拦截。
1. 检查浏览器请求URL与Controller中路径是否匹配。
2. 确认spring-mvc.xml<context:component-scan>的包路径包含了Controller。
3. 在spring-mvc.xml中配置<mvc:resources mapping="/static/**" location="/static/" />
插入数据库中文变成乱码1. 数据库/表字符集不是utf8mb4
2. 连接字符串未指定字符集。
3. 请求/响应编码未设置。
1. 确认MySQL数据库、表、字段字符集为utf8mb4
2. JDBC URL中添加?characterEncoding=utf8&useUnicode=true
3. 在web.xml中配置CharacterEncodingFilter,并设置forceEncodingtrue
事务@Transactional不生效1. 方法不是public
2. 异常被方法内部catch而未抛出。
3. 在同一个类内部方法调用(如A方法调用本类的B方法)。
1. Spring AOP代理基于接口或CGLIB,对public方法生效。
2. 确保异常能传播出去,或手动设置rollbackFor
3. 自调用会绕过代理,事务不生效。可将事务方法放到另一个Service中调用。
MyBatis查询结果字段为null1. 数据库字段名与实体类属性名不一致(大小写、下划线转驼峰)。
2.resultMap映射错误。
1. 在mybatis-config.xml中配置mapUnderscoreToCamelCase=true开启自动驼峰映射。
2. 检查SQL语句的别名或<resultMap>定义是否正确。
分页插件PageHelper失效1.PageHelper.startPage()调用位置不对。
2. 线程池环境下,分页参数被其他线程污染。
1.确保startPage紧邻Mapper查询方法,中间不能有其它查询。
2. 使用PageHelperPage参数模式,或确保在finally块中调用PageHelper.clearPage()清理线程变量。

调试必备技巧:

  1. 开启SQL日志:mybatis-config.xml中设置<setting name="logImpl" value="STDOUT_LOGGING" />,或在logback配置中将mapper包的日志级别设为DEBUG。这样可以在控制台看到MyBatis执行的所有SQL及其参数,是排查SQL问题最直接的手段。
  2. 善用Spring的@Slf4j注解:在Service、Controller中注入日志,在关键节点(方法入口、出口、异常处)打印入参、出参和关键变量。不要用System.out.println
  3. 使用Postman或ApiFox测试API:在开发RESTful API时,用这些工具模拟各种请求(GET/POST/PUT/DELETE),测试参数校验、异常处理、返回格式是否正确。
  4. 单元测试:为Service层关键方法编写JUnit单元测试,使用内存数据库(如H2)或Mockito模拟依赖。这能极大提升代码质量和重构信心。

构建一个完整的个人博客系统,是一个将分散的知识点串联成线的绝佳实践。从数据库设计、SSM框架整合、业务逻辑编写,到前端展示、安全优化,每一步都需要你思考和决策。当你的博客成功上线,并写下第一篇技术文章时,那种成就感是无可替代的。这个项目本身,也将成为你简历中一个扎实的、有说服力的作品。

本文还有配套的精品资源,点击获取

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

Android逆向入门:MT管理器核心功能与App目录结构详解

很多刚接触 Android 逆向或应用修改的朋友&#xff0c;可能都听说过“MT管理器”这个名字&#xff0c;但面对其复杂的界面和众多的功能按钮&#xff0c;往往感到无从下手。网上的教程要么过于零散&#xff0c;要么直接跳到高深的破解环节&#xff0c;对新手极不友好。本文将从零…

作者头像 李华
网站建设 2026/9/2 11:02:39

在 PC 上运行 Switch 游戏:Ryujinx 模拟器的安装、首通与调优实操

在 PC 上运行 Switch 游戏&#xff1a;Ryujinx 模拟器的安装、首通与调优实操 【免费下载链接】Ryujinx 用 C# 编写的实验性 Nintendo Switch 模拟器 项目地址: https://gitcode.com/GitHub_Trending/ry/Ryujinx 没有 Switch 主机&#xff0c;却想在电脑上玩它上面的游戏…

作者头像 李华
网站建设 2026/9/2 11:02:23

Python提取PDF表格:四大主流库对比与实战指南

在处理财务单据、科研论文、年报和合同的时候&#xff0c;PDF 里的表格是最让人头疼的部分&#xff1a;数据眼睛能看懂&#xff0c;程序却拿不到。很多人的第一反应是手动复制到 Excel&#xff0c;遇到几十页上百页的文件&#xff0c;或者每天都要处理批量 PDF&#xff0c;手动…

作者头像 李华
网站建设 2026/9/2 11:01:49

Diffusers:3 行代码跑通文生图的扩散模型工具箱

Diffusers&#xff1a;3 行代码跑通文生图的扩散模型工具箱 【免费下载链接】diffusers &#x1f917; Diffusers: State-of-the-art diffusion models for image, video, and audio generation in PyTorch. 项目地址: https://gitcode.com/GitHub_Trending/di/diffusers …

作者头像 李华
网站建设 2026/9/2 11:01:26

Orin 上 DeepStream 二次推理

Orin 上 DeepStream 二次推理 文章目录Orin 上 DeepStream 二次推理1. 结论先行2. 贯穿示例&#xff1a;四路门店 行人属性3. 主检测与二次推理的分工4. 二次推理在链路中的位置5. 什么时候该上二次推理6. 为二次网络准备 engine7. deepstream-app 配置要点8. batch-size 与路…

作者头像 李华
网站建设 2026/9/2 11:01:06

Umi-OCR 双层PDF转换指南:6步把扫描PDF变成可搜索文档

Umi-OCR 双层PDF转换指南&#xff1a;6步把扫描PDF变成可搜索文档 【免费下载链接】Umi-OCR OCR software, free and offline. 开源、免费的离线OCR软件。支持截屏/批量导入图片&#xff0c;PDF文档识别&#xff0c;排除水印/页眉页脚&#xff0c;扫描/生成二维码。内置多国语言…

作者头像 李华