JavaEE 环境与程序设计 · 交互课程

贯穿示例 · 留言本七版演进(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纯 JSPJSP scriptlet(jspWebModel/jspModel*.jsp)——起点:页面里直接写逻辑与 SQL
2JSP + ServletAddWordServlet + WordBean/WordsListBean把逻辑从 JSP 挪进 Servlet,数据用 Bean 承载(MVC 雏形)
3JDBCGuestBookDao 手写 JDBC数据库访问独立成 DAO,页面/Servlet 不再碰 Connection
4Spring IoCClassPathXmlApplicationContext对象由容器创建、依赖注入,不再到处 new
5SpringMVC@Controller + @RequestMapping前端控制器统一分发,注解替代手写路由
6SSMController → Service → Dao(MyBatis)三层分工 + 事务,各层职责清晰、可测
7SpringBoot@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类状态已注入属性
gb1Guestbook未创建—
gb2Guestbook未创建—
zUser未创建—
第 1 / 13 步

六、版本 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 接口
→
动态代理拦截
→
namespace+id 定位 SQL
→
参数映射
→
执行 SQL
→
结果映射成对象

调用 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:自动配置(对照省掉什么)

SSM 里干了什么

组件扫描(<context:component-scan>)+ DataSource + 声明式事务(<tx:annotation-driven>)

Boot 对应机制

自动配置:组件扫描 + 数据源 + 声明式事务,一并就绪

关键自动配置类 / starter

@SpringBootApplication(内含 @ComponentScan)· DataSourceAutoConfiguration · TransactionAutoConfiguration

✓ 省掉:不用手写 <context:component-scan>、<bean id="dataSource">、<tx:annotation-driven>

一句话:Boot = SSM 的「全自动档」——四份手写配置逐项交给自动配置类,部署从外部 Tomcat 变成内嵌容器 + java -jar。

九、小结:分层是「痛点逼出来的」

演进上一版的痛点这一版的答案
JSP → JSP+Servlet逻辑糊在页面里Servlet 管逻辑、Bean 装数据
→ JDBC DAO到处开 ConnectionDAO 独立封装数据访问
→ Spring IoC到处 new、硬依赖容器创建 + 依赖注入
→ SpringMVC手写路由 if-else@RequestMapping 注解分发
→ SSM层与层纠缠、无事务三层分工 + @Transactional
→ SpringBoot四份配置 + web.xml + 外部 Tomcat自动配置 + 内嵌容器,一个属性文件搞定

把这七版并排看,课程模块 1–11 就不再是十一个孤立知识点,而是一条「不断把耦合拆开」的演进路径。

学习小测验

Q1

版本 1(纯 JSP)最大的问题是( )

Q2

版本 3(JDBC)用 String.format 拼 SQL,其主要隐患是( )

Q3

版本 4 引入 ClassPathXmlApplicationContext 后,对象的创建由谁负责?

Q4

版本 6(SSM)中 @Service + @Transactional 所在的层负责( )

Q5

版本 7(SpringBoot)相比 SSM 版,最大的工程变化是( )