贯穿示例 · 留言本七版演进(JSP → SpringBoot)
同一个「留言板」功能,从纯 JSP 一路演进到 Spring+MVC+MyBatis 三层架构、再到 SpringBoot,看分层是怎么被逼出来的
贯穿示例 · 留言本七版演进(JSP → SpringBoot)
贯穿式教学示例(二):同一个「留言板」功能,用七个版本重写一遍。素材在本仓库同级的
GuestBook、GuestBookJDBC、GuestBookSpring、GuestBookSpringMVC、GuestBookSSM、GuestBookSpringBootMapper六个工程里(前两个阶段都在基础工程GuestBook内),下面代码均摘自真实源码(有删节)。这条线的意义:课程模块 1–11 分别讲 WEB、Servlet、JSP、Spring IoC、SpringMVC、MySQL/JDBC、MyBatis/SSM。本示例用一个不变的业务需求把它们串起来——你会看到,每多一层框架,都是为了解决上一版「写不下去」的痛点。
一、总览:同一功能,六种写法
| # | 版本 | 技术栈 | 解决的痛点 |
|---|---|---|---|
| 1 | 纯 JSP | JSP scriptlet(jspWebModel/jspModel*.jsp) | ——起点:页面里直接写逻辑与 SQL |
| 2 | JSP + Servlet | AddWordServlet + WordBean/WordsListBean | 把逻辑从 JSP 挪进 Servlet,数据用 Bean 承载(MVC 雏形) |
| 3 | JDBC | GuestBookDao 手写 JDBC | 数据库访问独立成 DAO,页面/Servlet 不再碰 Connection |
| 4 | Spring IoC | ClassPathXmlApplicationContext | 对象由容器创建、依赖注入,不再到处 new |
| 5 | SpringMVC | @Controller + @RequestMapping | 前端控制器统一分发,注解替代手写路由 |
| 6 | SSM | Controller → Service → Dao(MyBatis) | 三层分工 + 事务,各层职责清晰、可测 |
| 7 | SpringBoot | @SpringBootApplication + 内嵌 Tomcat + application.properties | 四份 XML / web.xml / 外部 Tomcat 被「约定大于配置」取代(模块 9) |
二、版本 1:纯 JSP——什么都写在页面里
起点 jspModel0.jsp 是典型的「JSP 什么都能干」:页面指令 + scriptlet,连取上下文路径都直接写:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()
+":"+request.getServerPort()+path+"/";
%>
问题一目了然:展示、业务、数据访问全糊在一个 .jsp 里,既难测也难维护。jspModel1/2 逐步把部分逻辑抽走,但根子没变。
三、版本 2:JSP + Servlet——MVC 雏形
把「处理请求」交给 Servlet,数据用 Bean 装:
// GuestBook/src/examples/AddWordServlet.java(节选)
public class AddWordServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
doPost(req, res); // GET/POST 都走同一套处理
}
}
配合 WordBean(单条留言)、WordsListBean(留言列表),已经能看出 M-V-C 的影子:JSP 是 View、Servlet 是 Controller、Bean 是 Model。但对象仍是一个个 new 出来的,彼此硬编码依赖。
四、版本 3:JDBC——数据访问独立成 DAO
GuestBookJDBC 把数据库访问抽到 GuestBookDao,手写 JDBC:
// GuestBookJDBC/src/com/un/GuestBookDao.java(节选)
public void saveGuestBook(GuestBook gb) {
String sql = String.format(
"insert into guestBook(username,title,contents) values('%s','%s','%s')",
gb.getUsername(), gb.getTitle(), gb.getContents());
db.executeUpdate(sql);
}
public List<GuestBook> queryGuestBook() {
ResultSet rs = db.executeQuery("select * from guestbook order by id desc");
// 逐行读 → 组装成 GuestBook 对象
}
注意这里 String.format 直接拼 SQL——能用,但有注入风险。这正是后面「为什么要 MyBatis 的参数占位符」的伏笔。(这一版即模块 7「JDBC 数据访问」的落点:五步连库 + DAO 封装。)
五、版本 4:Spring IoC——对象交给容器
GuestBookSpring 引入 Spring 容器,程序入口变成加载 XML 配置:
// GuestBookSpring/src/GuestbookApp.java(节选)
public static void main(String[] args) {
ApplicationContext ctx =
new ClassPathXmlApplicationContext("guestBookContext.xml");
// 之后从容器里 getBean(...),不再自己 new
}
依赖关系由 guestBookContext.xml 声明、由容器装配——「谁创建对象」这件事被抽走了。这是模块 4(Spring IoC/DI)的核心。
交互实验:IoC 容器如何创建并装配 Bean(IoCContainerSim)
业务代码里**没有一行 `new`**——对象由外部容器创建、持有、装配。点「下一步」,看容器按 「解析定义 → 实例化 → 依赖装配 → 就绪」四步把 guestBookContext.xml 里的 gb1 / gb2 / z 三个 bean 建出来并互相接好线(对照真实工程 GuestBookSpring/GuestbookApp.java)。
解析定义 · gb1 —— 读取 <bean id="gb1" class="com.un.Guestbook">
登记 bean 定义(id、类名),尚未创建对象
容器注册表(已创建的对象)
| bean id | 类 | 状态 | 已注入属性 |
|---|---|---|---|
| gb1 | Guestbook | 未创建 | — |
| gb2 | Guestbook | 未创建 | — |
| z | User | 未创建 | — |
六、版本 5:SpringMVC——注解驱动的前端控制器
GuestBookSpringMVC 用 @Controller + @RequestMapping 取代手写路由:
// GuestBookSpringMVC/src/com/un/controller/WordsController.java(节选)
@Controller
public class WordsController {
@RequestMapping("/index.do")
public ModelAndView helloWorld(HttpSession session) {
return new ModelAndView("words");
}
@RequestMapping("/addWord.do")
public String login(HttpServletRequest request, WordBean word) {
// 表单绑定的是单条留言 WordBean,加入应用属性里的列表后跳转
return "redirect:/index.do";
}
}
请求统一进前端控制器,按 @RequestMapping 分发到方法——模块 5(SpringMVC)的「注解映射 + 模型视图」在这里落地。
七、版本 6:SSM——三层分工 + 事务
GuestBookSSM 是终点:Controller → Service → Dao 三层,MyBatis 做数据访问,事务交给 Spring:
// GuestBookSSM/src/com/un/controller/guestbook/WordController.java(节选)
@Controller
@RequestMapping("/word")
public class WordController {
@Autowired private WordService wordService;
@RequestMapping("/list")
public String list(Model model) {
model.addAttribute("words", wordService.list()); // 交给 Service
return "guestbook";
}
}
// GuestBookSSM/src/com/un/service/guestbook/WordServiceImpl.java(节选)
@Service("wordService")
@Transactional
public class WordServiceImpl implements WordService {
@Autowired private WordDao wordDao;
public List<Word> list() { return wordDao.findAll(); }
public void addWord(Word w) { wordDao.save(w); }
}
三层各司其职:Controller 收请求、Service 管事务与业务、Dao(MyBatis Mapper)碰数据库。每一层都只依赖下一层的接口,可独立测试——这就是模块 10(SSM 整合)要达到的状态。
交互实验:MyBatis 一次查询全链路(MyBatisMapperFlow)
先选一个 DAO 方法,再点「下一步」,看一次 wordDao.findAll() 如何从接口调用一路走到 Java 对象。对照真实工程 GuestBookSSM(WordDao + WordMapper.xml)。
调用 DAO 接口 —— 负责组件 WordDao.findAll()
应用层像调普通 Java 方法一样调 wordDao.findAll()——接口里没有实现类,却能返回数据
#{...}:预编译占位符,MyBatis 把它转成 PreparedStatement 的 ?,参数与 SQL 分离,天然防 SQL 注入;${...}:字符串直接拼接进 SQL,用于动态表名/列名等无法预编译的场景,但必须校验输入,否则有注入风险。
八、版本 7:SpringBoot——约定大于配置 + 内嵌容器
GuestBookSpringBootMapper 把同一套 Controller → Service → Dao(MyBatis Mapper)业务搬进 SpringBoot:工程入口是一个 @SpringBootApplication 类,数据源 / 端口 / 事务全走 application.properties,不再需要外部 Tomcat 和 web.xml:
// GuestBookSpringBootMapper/src/main/java/WebAppPropSSMEx.java(节选)
@Configuration
@ComponentScan(basePackages = {"com.un.controller.guestbook","com.un.service.guestbook","com.un.config"})
@MapperScan("com.un.mapper.guestbook") // 扫描 MyBatis Mapper 接口,动态代理生成 Dao
@SpringBootApplication
@PropertySource("classpath:application.properties")
public class WebAppPropSSMEx {
public static void main(String[] args) {
SpringApplication.run(WebAppPropSSMEx.class, args); // 内嵌 Tomcat 直接跑
}
}
业务层与 SSM 版几乎一致——Controller 收请求、Service 管事务、Mapper 碰库:
// GuestBookSpringBootMapper/src/main/java/com/un/controller/guestbook/WordController.java(节选)
@Controller
@RequestMapping("/word")
public class WordController {
@Autowired private WordService wordService;
@RequestMapping("/list")
public String list(Model model, HttpSession session) {
List<Word> words = wordService.list();
model.addAttribute("words", words);
return "jsp/guestbook";
}
}
application.properties 里 server.port=8080、Druid 数据源、MyBatis / PageHelper 配置一目了然——「四份 XML + web.xml + 外部 Tomcat」被一个属性文件 + 内嵌容器取代。这正是模块 9(SpringBoot)要解决的死结。
SSM:手写装配(4 份配置 + web.xml)
SpringBoot:自动配置(对照省掉什么)
组件扫描(<context:component-scan>)+ DataSource + 声明式事务(<tx:annotation-driven>)
自动配置:组件扫描 + 数据源 + 声明式事务,一并就绪
@SpringBootApplication(内含 @ComponentScan)· DataSourceAutoConfiguration · TransactionAutoConfiguration
一句话:Boot = SSM 的「全自动档」——四份手写配置逐项交给自动配置类,部署从外部 Tomcat 变成内嵌容器 + java -jar。
九、小结:分层是「痛点逼出来的」
| 演进 | 上一版的痛点 | 这一版的答案 |
|---|---|---|
| JSP → JSP+Servlet | 逻辑糊在页面里 | Servlet 管逻辑、Bean 装数据 |
| → JDBC DAO | 到处开 Connection | DAO 独立封装数据访问 |
| → Spring IoC | 到处 new、硬依赖 | 容器创建 + 依赖注入 |
| → SpringMVC | 手写路由 if-else | @RequestMapping 注解分发 |
| → SSM | 层与层纠缠、无事务 | 三层分工 + @Transactional |
| → SpringBoot | 四份配置 + web.xml + 外部 Tomcat | 自动配置 + 内嵌容器,一个属性文件搞定 |
把这七版并排看,课程模块 1–11 就不再是十一个孤立知识点,而是一条「不断把耦合拆开」的演进路径。
学习小测验
版本 1(纯 JSP)最大的问题是( )
版本 3(JDBC)用 String.format 拼 SQL,其主要隐患是( )
版本 4 引入 ClassPathXmlApplicationContext 后,对象的创建由谁负责?
版本 6(SSM)中 @Service + @Transactional 所在的层负责( )
版本 7(SpringBoot)相比 SSM 版,最大的工程变化是( )