从JDBC到SpringBoot:Java后端开发的框架进化之路

最近带了个新人,让他写一个简单的用户查询接口。他花了两天时间,交上来一堆代码——JDBC连接、手动映射结果集、异常处理、连接关闭……代码量比业务逻辑多了三倍。我看了之后感慨万千,想起自己刚入行时也是这么干的。后来一步步从MyBatis到Spring再到SpringBoot,才真正体会到"框架"这两个字的分量。这篇文章就把我这些年对SSM和SpringBoot的理解整理出来,希望能帮到和我当初一样迷茫的同学。


一、先说说JDBC的痛

在没有框架的年代,我们操作数据库全靠JDBC。写一个简单的查询,代码长这样:

// 原生JDBC查询——光是连接管理就占了一大半代码
public Dept findByDeptno(int deptno) {
    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs = null;
    Dept dept = null;
    try {
        // 1. 加载驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 2. 获取连接
        conn = DriverManager.getConnection(
            "jdbc:mysql://localhost:3306/mydb", "root", "root");
        // 3. 编写SQL
        String sql = "SELECT * FROM dept WHERE deptno = ?";
        ps = conn.prepareStatement(sql);
        ps.setInt(1, deptno);
        // 4. 执行查询
        rs = ps.executeQuery();
        // 5. 手动映射结果集到对象——这一步最烦
        if (rs.next()) {
            dept = new Dept();
            dept.setDeptno(rs.getInt("deptno"));
            dept.setDname(rs.getString("dname"));
            dept.setLoc(rs.getString("loc"));
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 6. 关闭资源——每个方法都要写一遍
        try { if (rs != null) rs.close(); } catch (Exception e) {}
        try { if (ps != null) ps.close(); } catch (Exception e) {}
        try { if (conn != null) conn.close(); } catch (Exception e) {}
    }
    return dept;
}

这段代码有几个问题,相信写过的人都有体会:

第一,太啰嗦。 一个简单的查询,业务逻辑只有3行(设置参数、执行、映射),剩下的全是样板代码。

第二,手动映射结果集。 数据库的列名和Java对象的属性名要一个个对应起来。表有20个字段,你就得写20行getXxx。字段多了,漏一个都不好排查。

第三,没有连接池。 每次查询都创建新连接、关闭连接,数据库压力山大。你可能会说"我自己搞个连接池不就行了",没错,但这就是在造轮子了。

第四,SQL和Java代码耦合。 SQL语句写死在Java代码里,改SQL就得改Java代码、重新编译。DBA想帮你优化SQL都无从下手。

这些问题不是不能解决,而是每个项目都要解决一遍。于是,框架来了。


二、MyBatis——让SQL回归纯粹

2.1 先搞清楚两个概念

在聊MyBatis之前,有两个概念需要先说清楚:

ORM(Object-Relation Mapping,对象关系映射):简单说,就是把数据库表和Java对象建立一个映射关系。查出来的每一行数据自动变成一个Java对象,你不用再手动rs.getXxx了。

持久层(Persistence Layer):专门负责把数据保存到数据库(或从数据库读出来)的那一层代码。以前用JDBC写的DAO层,后来用MyBatis写的Mapper层,都是持久层。

2.2 MyBatis是什么

MyBatis是一款半自动的ORM框架。注意这个"半自动"——它不像Hibernate那样完全帮你把SQL生成了,而是让你自己写SQL,它负责帮你把结果集映射成对象、帮你管理连接和参数。

你可以把它理解为:JDBC的升级版,SQL还是你写,但脏活累活它干了。

它的核心思想很简单:把SQL写在XML文件里(或者用注解),Java代码只负责调用。SQL和Java代码分离,各管各的。

2.3 第一个MyBatis程序

说了这么多,来点实际的。用MyBatis分三步走:

第一步:导入依赖

<!-- pom.xml -->
<dependencies>
    <!-- MyBatis核心包 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.3</version>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.16</version>
    </dependency>
    <!-- Lombok——自动生成getter/setter,省得手写 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

第二步:写实体类和Mapper映射文件

// 实体类——对应数据库的dept表
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Dept implements Serializable {
    private Integer deptno;  // 部门编号
    private String dname;    // 部门名称
    private String loc;      // 部门地址
}
<!-- DeptMapper.xml——SQL写在这里,和Java代码彻底分离 -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace用来区分不同的Mapper文件,避免SQL id冲突 -->
<mapper namespace="DeptMapper">
    <!--
        id: 唯一标识,调用时用这个名字
        resultType: 查询结果每条记录映射成什么类型
        注意:即使返回List,resultType也写集合中元素的类型,不是List本身
    -->
    <select id="findAll" resultType="com.msb.pojo.Dept">
        select * from dept
    </select>
</mapper>

第三步:核心配置文件

<!-- sqlMapConfig.xml——MyBatis的总配置文件 -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 数据库连接信息 -->
    <environments default="development">
        <environment id="development">
            <!-- JDBC事务管理——MyBatis自己管事务 -->
            <transactionManager type="JDBC"/>
            <!-- POOLED表示使用连接池,解决了JDBC频繁创建/销毁连接的问题 -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&amp;serverTimezone=Asia/Shanghai"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 告诉MyBatis去哪找Mapper映射文件 -->
    <mappers>
        <mapper resource="com/msb/mapper/DeptMapper.xml"/>
    </mappers>
</configuration>

配置搞定,接下来写测试代码:

public class Test1 {
    private SqlSession sqlSession;

    @Before
    public void init() {
        // SqlSessionFactoryBuilder:用完就扔的建造者
        SqlSessionFactoryBuilder ssfb = new SqlSessionFactoryBuilder();
        // 加载核心配置文件
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        // SqlSessionFactory:全局单例,创建SqlSession的工厂
        SqlSessionFactory factory = ssfb.build(resourceAsStream);
        // SqlSession:每次数据库操作都需要它,用完要关闭
        sqlSession = factory.openSession();
    }

    @Test
    public void testFindAll() {
        // 调用SQL——参数是"namespace.id"
        List<Dept> list = sqlSession.selectList("DeptMapper.findAll");
        for (Dept dept : list) {
            System.out.println(dept);
        }
    }

    @After
    public void release() {
        // 用完记得关闭,释放连接回连接池
        sqlSession.close();
    }
}

对比一下之前的JDBC代码:没有Class.forName、没有DriverManager.getConnection、没有rs.getXxx、没有finally关闭资源。同样的功能,代码量少了三分之二。 这就是框架的价值。

2.4 参数传递的三种方式

实际开发中,查询肯定要带参数。MyBatis支持三种传参方式:

方式一:单个基本类型

<!-- 根据工号查询员工 -->
<select id="findByEmpno" resultType="emp" parameterType="int">
    <!-- #{} 是参数占位符,类似JDBC的?,名字随便写,见名知意即可 -->
    select * from emp where empno = #{empno}
</select>

方式二:Map集合

当参数有多个、且不是同一个对象的属性时,用Map最合适:

// Java代码中组装参数
Map<String, Object> args = new HashMap<>();
args.put("deptno", 20);
args.put("sal", 1500.0);
List<Emp> emps = sqlSession.selectList("findEmpByDeptnoAndSal", args);
<select id="findEmpByDeptnoAndSal" resultType="emp" parameterType="map">
    <!-- #{}里写Map的key -->
    select * from emp where deptno = #{deptno} and sal >= #{sal}
</select>

方式三:引用类型(对象)

参数本身就是某个对象的属性时,直接传对象:

<!-- 参数是Emp对象,#{}里写属性名 -->
<select id="findEmpByDeptnoAndSal2" resultType="emp" parameterType="emp">
    select * from emp where deptno = #{deptno} and sal >= #{sal}
</select>

这里有个小细节:#{}${}都能做参数占位,但区别很大。#{}用的是PreparedStatement(预编译,防SQL注入),${}用的是字符串拼接(有注入风险)。日常开发用#{}就对了,${}只在需要动态拼接表名、列名时才考虑。

2.5 动态SQL——MyBatis的杀手锏

写过复杂查询的人都知道,查询条件经常是动态的——用户可能只填了姓名,也可能姓名和部门都填了,也可能什么都不填查全部。用JDBC写这种逻辑,免不了一堆if-else拼接SQL字符串。

MyBatis用XML标签优雅地解决了这个问题:

if标签——按条件拼接SQL

<!-- 动态查询:根据传入的条件有选择地拼接WHERE子句 -->
<select id="findEmpByCondition" resultType="emp" parameterType="emp">
    select * from emp where 1=1
    <!-- test属性写判断条件,为true才拼接这段SQL -->
    <if test="ename != null and ename != ''">
        and ename = #{ename}
    </if>
    <if test="deptno != null">
        and deptno = #{deptno}
    </if>
    <if test="sal != null">
        and sal >= #{sal}
    </if>
</select>

不过上面的where 1=1看着有点蠢。MyBatis提供了where标签来解决:

where标签——自动处理AND/OR

<select id="findEmpByCondition" resultType="emp" parameterType="emp">
    select * from emp
    <!-- where标签会自动去掉多余的AND/OR,还会自动添加WHERE关键字 -->
    <where>
        <if test="ename != null and ename != ''">
            and ename = #{ename}
        </if>
        <if test="deptno != null">
            and deptno = #{deptno}
        </if>
    </where>
</select>

choose标签——多选一,类似Java的switch

<select id="findEmpByChoose" resultType="emp" parameterType="emp">
    select * from emp
    <where>
        <choose>
            <!-- when的顺序很重要,命中第一个就不再往下走了 -->
            <when test="empno != null">
                empno = #{empno}
            </when>
            <when test="ename != null and ename != ''">
                ename = #{ename}
            </when>
            <otherwise>
                deptno = 10
            </otherwise>
        </choose>
    </where>
</select>

foreach标签——遍历集合

批量操作是常见需求,比如IN查询:

<!-- 根据多个工号查询员工 -->
<select id="findByEmpnos" resultType="emp">
    select * from emp where empno in
    <!-- collection: 要遍历的集合  item: 每次遍历的元素  separator: 分隔符 -->
    <foreach collection="list" item="empno" open="(" close=")" separator=",">
        #{empno}
    </foreach>
</select>

这段配置最终生成的SQL类似:select * from emp where empno in (7369, 7499, 7521)

set标签——动态更新

更新操作也有同样的问题:用户可能只改了名字,也可能改了名字和薪资。set标签自动处理多余的逗号:

<update id="updateEmp" parameterType="emp">
    update emp
    <set>
        <if test="ename != null">ename = #{ename},</if>
        <if test="sal != null">sal = #{sal},</if>
        <if test="job != null">job = #{job},</if>
    </set>
    where empno = #{empno}
</update>

动态SQL是MyBatis最实用的特性之一。它让你不用在Java代码里拼SQL字符串,逻辑清晰、不容易出错。

2.6 多表关联查询

单表查询搞定了,但实际业务中经常需要关联查询。比如查员工信息时要同时显示部门名称,这就涉及到表关联。

手动映射结果集(resultMap)

当列名和属性名不一致,或者关联查询需要映射到多个对象时,用resultMap手动指定映射关系:

<!-- resultMap:手动定义列和属性的映射关系 -->
<resultMap id="empDeptMap" type="emp">
    <!-- 主键用id标签,普通列用result标签 -->
    <id property="empno" column="empno"/>
    <result property="ename" column="ename"/>
    <result property="job" column="job"/>
    <!--
        association:处理一对一关联
        property: Emp对象中关联的Dept属性名
        javaType: 关联对象的类型
    -->
    <association property="dept" javaType="dept">
        <id property="deptno" column="deptno"/>
        <result property="dname" column="dname"/>
        <result property="loc" column="loc"/>
    </association>
</resultMap>

<select id="findEmpWithDept" resultMap="empDeptMap">
    select e.*, d.dname, d.loc
    from emp e
    left join dept d on e.deptno = d.deptno
</select>

一对多的场景(比如查部门信息时带上该部门所有员工),用collection标签:

<resultMap id="deptEmpMap" type="dept">
    <id property="deptno" column="deptno"/>
    <result property="dname" column="dname"/>
    <result property="loc" column="loc"/>
    <!-- collection:处理一对多关联,ofType是集合中元素的类型 -->
    <collection property="emps" ofType="emp">
        <id property="empno" column="empno"/>
        <result property="ename" column="ename"/>
        <result property="job" column="job"/>
    </collection>
</resultMap>

2.7 MyBatis代理模式——告别手动调用

前面的例子里,我们是通过sqlSession.selectList("DeptMapper.findAll")来调用SQL的。这种方式有个问题:字符串参数没有编译期检查,写错了只有运行时才发现。

MyBatis的代理模式解决了这个问题:定义一个Java接口,MyBatis自动生成实现类。

// Mapper接口——方法名要和XML中的SQL id一致
public interface DeptMapper {
    List<Dept> findAll();
    Dept findByDeptno(int deptno);
    int insertDept(Dept dept);
    int updateDept(Dept dept);
    int deleteDept(int deptno);
}
<!-- namespace改成接口的全限定名 -->
<mapper namespace="com.msb.mapper.DeptMapper">
    <select id="findAll" resultType="dept">
        select * from dept
    </select>
    <select id="findByDeptno" resultType="dept">
        select * from dept where deptno = #{deptno}
    </select>
</mapper>

使用时直接调用接口方法:

// 通过getMapper获取代理对象
DeptMapper mapper = sqlSession.getMapper(DeptMapper.class);
// 直接调用接口方法——有编译期检查,IDE可以跳转
List<Dept> depts = mapper.findAll();
Dept dept = mapper.findByDeptno(10);

代理模式的好处:类型安全、IDE友好、代码更简洁。这也是目前MyBatis最主流的使用方式。

2.8 缓存——避免重复查询

MyBatis内置了两级缓存,用来提升查询性能:

一级缓存:SqlSession级别的缓存,同一个SqlSession中执行相同的查询,第二次直接从缓存取。默认开启,不用配置。

二级缓存:Mapper级别的缓存,多个SqlSession共享同一个Mapper的缓存。需要手动开启:

<!-- 在Mapper映射文件中添加cache标签 -->
<cache/>
<!-- 或者更详细的配置 -->
<cache
    eviction="FIFO"          <!-- 缓存淘汰策略:先进先出 -->
    flushInterval="60000"    <!-- 每60秒刷新一次 -->
    size="512"               <!-- 最多缓存512个对象 -->
    readOnly="true"/>        <!-- 只读,防止修改缓存中的对象 -->

补充一下:缓存淘汰策略除了FIFO(先进先出),还有LRU(最近最少使用,默认策略)、SOFT(软引用)、WEAK(弱引用)等。实际项目中,MyBatis的二级缓存用得不多,更多是用Redis来做缓存,因为Redis支持分布式、持久化、更灵活的过期策略。


三、Spring——Java开发的基石

3.1 为什么需要Spring

MyBatis解决了持久层的问题,但一个完整的Java应用远不止数据库操作。你还需要管理对象的创建、处理对象之间的依赖关系、实现日志记录、事务控制等等。

假设有一个UserService,它依赖UserDao来操作数据库:

// 没有Spring时,你得自己new
public class UserService {
    private UserDao userDao = new UserDaoImpl();  // 硬编码了具体实现

    public User findById(int id) {
        return userDao.findById(id);
    }
}

这段代码有什么问题?UserServiceUserDaoImpl强耦合了。如果有一天要换成UserDaoMybatisImpl,你得改UserService的代码。一个项目几百个类,改起来要命。

Spring的核心思想就是:你别自己new了,我来帮你创建和管理对象。

3.2 IOC——控制反转

IOC(Inversion of Control,控制反转)是Spring最核心的概念。通俗地说:以前是你自己创建对象(new UserDaoImpl()),现在是把创建对象的权力交给Spring容器。

为什么要反转? 因为对象的创建和管理不应该由使用者来做。就像你去餐厅吃饭,你不需要自己买菜、做菜,你只需要点菜(声明依赖),厨师(Spring)帮你搞定。

用XML配置的方式告诉Spring"帮我管理这些对象":

<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 注册UserDaoImpl到Spring容器,id是它的名字 -->
    <bean id="userDao" class="com.msb.dao.UserDaoImpl"/>

    <!-- 注册UserService,并注入userDao依赖 -->
    <bean id="userService" class="com.msb.service.UserService">
        <!-- property标签:通过setter方法注入userDao -->
        <property name="userDao" ref="userDao"/>
    </bean>
</beans>

使用时从容器中获取对象:

// 加载配置文件,启动Spring容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
// 从容器中获取对象——不需要new了
UserService userService = ctx.getBean("userService", UserService.class);
User user = userService.findById(1);

3.3 DI——依赖注入

DI(Dependency Injection,依赖注入)是IOC的具体实现方式。Spring创建对象时,自动把依赖的其他对象"注入"进来。

注入方式有三种:

构造器注入(推荐):

public class UserService {
    private final UserDao userDao;

    // 通过构造器注入——依赖一目了然,而且是不可变的
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}
<bean id="userService" class="com.msb.service.UserService">
    <constructor-arg ref="userDao"/>
</bean>

Setter注入

public class UserService {
    private UserDao userDao;

    // 通过setter注入——依赖可变,但配置灵活
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

注解注入(最常用):

@Service  // 告诉Spring"请管理这个类"
public class UserService {
    @Autowired  // 自动从容器中找到UserDao类型的对象注入进来
    private UserDao userDao;
}

关于@Autowired:它是按类型匹配的。如果容器中有多个同类型的Bean,可以配合@Qualifier指定名称,或者用@Resource按名称注入。

3.4 Bean的生命周期

Spring管理的对象叫做Bean。一个Bean从创建到销毁,会经历一系列步骤:

1. 实例化(调用构造器)
   ↓
2. 属性赋值(依赖注入)
   ↓
3. BeanNameAware / BeanFactoryAware(感知容器信息,了解即可)
   ↓
4. BeanPostProcessor.postProcessBeforeInitialization(前置处理)
   ↓
5. 初始化(@PostConstruct 或 init-method)
   ↓
6. BeanPostProcessor.postProcessAfterInitialization(后置处理,AOP代理就在这里生成)
   ↓
7. 使用Bean
   ↓
8. 销毁(@PreDestroy 或 destroy-method)

了解Bean的生命周期很重要,它能帮你理解Spring AOP是怎么实现的、@PostConstruct为什么能做初始化工作等等。面试也经常问。

3.5 AOP——面向切面编程

AOP(Aspect-Oriented Programming,面向切面编程)是Spring的另一大核心。它解决的是"横切关注点"的问题——那些散布在多个方法中的通用逻辑,比如日志记录、事务管理、权限校验。

举个例子:你有100个Service方法,每个方法执行前后都要记录日志。没有AOP,你得在每个方法里都写一遍日志代码:

// 没有AOP——日志代码到处重复
public void addUser(User user) {
    log.info("开始执行addUser,参数:{}", user);  // 重复
    userDao.insert(user);
    log.info("addUser执行完成");  // 重复
}

public void deleteUser(int id) {
    log.info("开始执行deleteUser,参数:{}", id);  // 重复
    userDao.delete(id);
    log.info("deleteUser执行完成");  // 重复
}

有了AOP,你可以把日志逻辑抽到一个"切面"里,让它自动"织入"到目标方法的前后:

@Aspect  // 标记这是一个切面类
@Component
public class LogAspect {

    // 定义切入点:com.msb.service包下所有类的所有方法
    @Pointcut("execution(* com.msb.service.*.*(..))")
    public void servicePointcut() {}

    // 前置通知:目标方法执行前执行
    @Before("servicePointcut()")
    public void before(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        log.info("开始执行{},参数:{}", methodName, args);
    }

    // 后置通知:目标方法执行后执行
    @After("servicePointcut()")
    public void after(JoinPoint joinPoint) {
        log.info("{}执行完成", joinPoint.getSignature().getName());
    }
}

AOP的底层原理是动态代理。Spring会为目标类生成一个代理对象,调用方法时先执行切面逻辑,再调用目标方法。Spring默认对实现了接口的类使用JDK动态代理(基于接口),对没有实现接口的类使用CGLIB代理(基于继承)。

JDK动态代理和CGLIB代理的区别:JDK动态代理要求目标类必须实现接口,它代理的是接口;CGLIB通过生成目标类的子类来实现代理,不需要接口,但不能代理final类和方法。Spring Boot 2.x之后默认使用CGLIB。

3.6 事务管理

事务是数据库操作中绕不开的话题。简单回顾一下:事务是一组操作,要么全部成功,要么全部失败。它有四个特性(ACID):原子性、一致性、隔离性、持久性。

Spring提供了声明式事务管理,让你用注解就能控制事务:

@Service
public class TransferService {

    @Autowired
    private AccountMapper accountMapper;

    /**
     * 转账——经典的事务场景
     * A转B钱,扣A的和加B的必须同时成功或同时失败
     */
    @Transactional  // 这个方法在一个事务中执行
    public void transfer(int fromId, int toId, BigDecimal amount) {
        // 扣减A的余额
        Account from = accountMapper.selectById(fromId);
        from.setBalance(from.getBalance().subtract(amount));
        accountMapper.updateById(from);

        // 增加B的余额
        Account to = accountMapper.selectById(toId);
        to.setBalance(to.getBalance().add(amount));
        accountMapper.updateById(to);

        // 如果中间抛异常,整个事务回滚,A的钱不会少,B的钱也不会多
    }
}

@Transactional注解的常用属性:

@Transactional(
    propagation = Propagation.REQUIRED,  // 传播行为:当前有事务就加入,没有就新建
    isolation = Isolation.DEFAULT,       // 隔离级别:使用数据库默认级别
    timeout = 30,                        // 超时时间:30秒
    readOnly = false,                    // 是否只读:查询方法可以设为true提升性能
    rollbackFor = Exception.class        // 遇到什么异常回滚:默认只回滚RuntimeException
)

事务传播行为(Propagation)是个容易被忽略但很重要的概念。REQUIRED是最常用的——如果当前已经有事务,就加入;没有就新建一个。REQUIRES_NEW则总是新建事务,暂停当前事务。嵌套调用时传播行为决定了事务的边界,搞错了会导致数据不一致。


四、SpringMVC——Web层的利器

4.1 从Servlet到SpringMVC

在没有SpringMVC之前,Java Web开发是基于Servlet的。每个请求对应一个Servlet,你需要在web.xml中配置大量的映射关系,而且Servlet的API非常繁琐:

// 原生Servlet——一个接口就要写这么多代码
@WebServlet("/user")
public class UserServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        // 获取参数
        String idStr = req.getParameter("id");
        int id = Integer.parseInt(idStr);
        // 调用Service
        UserService userService = new UserService();
        User user = userService.findById(id);
        // 设置响应
        req.setAttribute("user", user);
        req.getRequestDispatcher("/user.jsp").forward(req, resp);
    }
}

SpringMVC对Servlet进行了封装,让你用注解就能定义请求映射:

@Controller  // 标记这是一个控制器
@RequestMapping("/user")  // 所有接口的URL前缀
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 根据ID查询用户
     * 访问地址:GET /user/1
     */
    @GetMapping("/{id}")  // @GetMapping = @RequestMapping(method = GET)
    public String findById(@PathVariable("id") int id, Model model) {
        User user = userService.findById(id);
        model.addAttribute("user", user);
        return "user";  // 返回视图名称,跳转到user.jsp
    }

    /**
     * 新增用户——接收JSON请求体
     */
    @PostMapping  // POST /user
    @ResponseBody  // 直接返回数据,不走视图解析
    public Result addUser(@RequestBody User user) {
        userService.addUser(user);
        return Result.success();
    }
}

4.2 常用注解速览

SpringMVC的注解很多,这里列几个最常用的:

注解 作用 示例
@Controller 标记控制器类 @Controller
@RestController = @Controller + @ResponseBody 返回JSON时用
@RequestMapping 映射请求URL @RequestMapping("/api")
@GetMapping GET请求映射 @GetMapping("/{id}")
@PostMapping POST请求映射 @PostMapping
@PathVariable 从URL路径中取参数 @PathVariable("id")
@RequestParam 从查询参数中取值 @RequestParam("name")
@RequestBody 接收JSON请求体 @RequestBody User user
@ResponseBody 返回JSON响应 方法上加
@CrossOrigin 允许跨域访问 类或方法上加

4.3 SSM整合

SSM(Spring + SpringMVC + MyBatis)是Java Web的经典架构。整合的核心思路是:Spring是大管家,SpringMVC负责Web层,MyBatis负责持久层。

整合配置的关键点:

<!-- applicationContext.xml——Spring的配置 -->
<!-- 1. 数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driver}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<!-- 2. SqlSessionFactory——MyBatis和Spring的桥梁 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    <property name="typeAliasesPackage" value="com.msb.pojo"/>
</bean>

<!-- 3. Mapper扫描——自动创建Mapper接口的代理对象 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.msb.mapper"/>
</bean>

<!-- 4. 包扫描——自动注册Service、Controller等组件 -->
<context:component-scan base-package="com.msb"/>

<!-- 5. 事务管理器 -->
<bean id="transactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!-- 开启注解驱动的事务管理 -->
<tx:annotation-driven transaction-manager="transactionManager"/>

SSM整合的目录结构通常是这样的:

src/main/java/
├── com.msb.pojo/        # 实体类
├── com.msb.mapper/      # Mapper接口(MyBatis)
├── com.msb.service/     # Service层(Spring管理)
│   └── impl/            # Service实现类
├── com.msb.controller/  # Controller层(SpringMVC)
└── com.msb.config/      # 配置类
src/main/resources/
├── mapper/              # Mapper XML文件
├── applicationContext.xml
├── springmvc.xml
└── jdbc.properties

说实话,SSM整合的配置确实繁琐。光是XML配置文件就好几个,每个文件里都是一堆bean定义。这也是为什么后来SpringBoot一出来就火了——它把这些配置都自动化了。


五、SpringBoot——约定大于配置

5.1 SpringBoot解决什么问题

SSM整合虽然功能强大,但配置太复杂了。一个新项目搭建环境就要花半天:配数据源、配事务管理器、配Mapper扫描、配视图解析器……而且不同项目的配置大同小异,重复劳动。

SpringBoot的核心理念是约定大于配置。它通过自动配置(Auto-Configuration)帮你把大部分配置都做好了,你只需要关注业务代码。

5.2 搭建SpringBoot项目

方式一:使用Spring Initializr(推荐)

访问 https://start.spring.io ,选择需要的依赖,下载项目压缩包,解压导入IDEA即可。

方式二:手动创建Maven项目

<!-- pom.xml——继承SpringBoot父项目,版本管理全交给它 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
</parent>

<dependencies>
    <!-- spring-boot-starter-web:内置了Tomcat、SpringMVC、Jackson等 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- mybatis-spring-boot-starter:MyBatis和SpringBoot的整合包 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.3.1</version>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
</dependencies>

启动类:

@SpringBootApplication  // 这个注解包含了@ComponentScan和自动配置
public class Application {
    public static void main(String[] args) {
        // 一行代码启动整个应用——SSM时代想都不敢想
        SpringApplication.run(Application.class, args);
    }
}

就这么多。没有XML配置文件,没有web.xml,没有applicationContext.xml。启动类一跑,内嵌的Tomcat自动启动,SpringMVC自动配置好,Mapper自动扫描好。

5.3 application.yml——唯一的配置文件

SpringBoot把所有配置集中到一个文件里:

# application.yml——SpringBoot的统一配置文件
server:
  port: 8080  # 服务端口号

spring:
  datasource:
    # 数据库连接信息——SpringBoot自动配置数据源
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root

mybatis:
  # Mapper XML文件的位置
  mapper-locations: classpath:mapper/*.xml
  # 实体类别名包——这样XML中resultType可以直接写类名
  type-aliases-package: com.msb.pojo
  configuration:
    # 开启驼峰命名自动映射(数据库下划线命名 → Java驼峰命名)
    map-underscore-to-camel-case: true

对比SSM时代的好几个XML配置文件,SpringBoot只需要一个application.yml,而且格式清晰、层次分明。

5.4 整合MyBatis

SpringBoot整合MyBatis非常简单,加了starter之后,只需要两步:

第一步:在Mapper接口上加@Mapper注解

@Mapper  // 告诉SpringBoot这是MyBatis的Mapper接口,需要生成代理对象
public interface UserMapper {

    @Select("SELECT * FROM user WHERE id = #{id}")
    User findById(int id);

    @Insert("INSERT INTO user(name, age) VALUES(#{name}, #{age})")
    int insert(User user);

    // 复杂SQL还是建议写在XML中
    List<User> findByCondition(UserQuery query);
}

简单SQL可以用注解,复杂SQL还是写XML。两种方式可以混用。

第二步:启动类上加@MapperScan(可选)

@SpringBootApplication
@MapperScan("com.msb.mapper")  // 批量扫描Mapper接口,不用每个都加@Mapper
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

5.5 SpringBoot的启动原理

SpringBoot为什么能"零配置"启动?核心是@SpringBootApplication注解,它是一个组合注解:

@SpringBootApplication
  ├── @SpringBootConfiguration  // 标记这是一个配置类
  ├── @EnableAutoConfiguration  // 开启自动配置——核心!
  │     └── @Import(AutoConfigurationImportSelector.class)// 读取META-INF/spring.factories中的自动配置类// 根据classpath中的jar包和配置,自动判断需要哪些配置
  └── @ComponentScan  // 包扫描

自动配置的原理:SpringBoot在启动时会扫描META-INF/spring.factories文件,里面列出了所有可自动配置的类。每个自动配置类都有条件注解(@ConditionalOnClass@ConditionalOnProperty等),只有满足条件才会生效。

比如你引入了spring-boot-starter-web,classpath里有了DispatcherServlet类,DispatcherServletAutoConfiguration就会自动生效,帮你创建DispatcherServlet的Bean。这就是"约定大于配置"的实现方式。

5.6 整合其他组件

SpringBoot整合各种组件的模式都一样:加依赖 → 写配置 → 用注解。

整合分页插件PageHelper

<!-- 加依赖 -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.7</version>
</dependency>
// 使用——两行代码搞定分页
@GetMapping("/users")
public PageInfo<User> list(@RequestParam(defaultValue = "1") int pageNum,
                           @RequestParam(defaultValue = "10") int pageSize) {
    // 开启分页:告诉MyBatis接下来的查询要分页
    PageHelper.startPage(pageNum, pageSize);
    // 执行查询——MyBatis会自动加上LIMIT子句
    List<User> users = userMapper.findAll();
    // 包装成分页信息(总条数、总页数、当前页数据等)
    return new PageInfo<>(users);
}

整合Druid连接池

# application.yml中配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      initial-size: 5       # 初始连接数
      max-active: 20        # 最大连接数
      min-idle: 5           # 最小空闲连接
      max-wait: 60000       # 获取连接最大等待时间(毫秒)

整合日志框架Logback

SpringBoot默认集成了Logback,不需要额外依赖。只需要在resources下加一个logback-spring.xml

<configuration>
    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- 日志级别:SpringBoot默认INFO,开发时可以调成DEBUG -->
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
    <!-- 单独设置某个包的日志级别——排查问题时很有用 -->
    <logger name="com.msb.mapper" level="DEBUG"/>
</configuration>

5.7 模板引擎——Thymeleaf

早期Java Web用JSP做页面渲染,但JSP依赖Servlet容器,SpringBoot用的是内嵌Tomcat,对JSP支持不太好。SpringBoot推荐使用模板引擎,其中Thymeleaf是最流行的选择。

Thymeleaf是一种HTML模板引擎——你在HTML中用特殊属性标记动态内容,服务器渲染后返回标准HTML给浏览器。

<!-- user.html——Thymeleaf模板 -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>用户信息</title>
</head>
<body>
    <!-- th:text:输出文本内容 -->
    <h1 th:text="${user.name}">用户名</h1>

    <!-- th:if:条件渲染 -->
    <p th:if="${user.age > 18}">已成年</p>

    <!-- th:each:循环遍历 -->
    <table>
        <tr th:each="item : ${users}">
            <td th:text="${item.name}">姓名</td>
            <td th:text="${item.age}">年龄</td>
        </tr>
    </table>
</body>
</html>

Controller中返回模板名称即可:

@Controller  // 注意:用Thymeleaf时用@Controller,不是@RestController
public class UserController {

    @GetMapping("/user/{id}")
    public String userDetail(@PathVariable int id, Model model) {
        User user = userService.findById(id);
        model.addAttribute("user", user);
        return "user";  // 返回模板名称,Thymeleaf自动找templates/user.html
    }
}

JSP、FreeMarker、Thymeleaf都是模板引擎,作用类似:把动态数据填充到HTML中。区别在于语法和特性。JSP是Servlet时代的产物,依赖Servlet容器;FreeMarker功能强大但学习曲线陡;Thymeleaf是SpringBoot官方推荐,语法简洁,可以直接在浏览器中打开预览(不启动服务器),开发体验最好。


六、回顾与总结

从JDBC到SpringBoot,Java后端开发经历了几次重大进化:

阶段 解决的问题 新增的复杂度
JDBC 最基础的数据库操作 样板代码多、手动映射、无连接池
MyBatis SQL与Java分离、自动映射、动态SQL 需要配置XML
Spring 对象管理、依赖注入、AOP、事务 XML配置繁琐
SpringMVC Web层封装、注解驱动 SSM整合配置复杂
SpringBoot 自动配置、约定大于配置、快速启动 几乎没有

每一次进化都是为了解决前一阶段的痛点:

  1. JDBC太啰嗦 → MyBatis帮你封装,SQL写在XML里,结果集自动映射
  2. 对象管理太乱 → Spring帮你创建和管理对象,依赖自动注入
  3. 通用逻辑太散 → Spring AOP帮你把日志、事务等横切逻辑集中管理
  4. Web开发太繁琐 → SpringMVC用注解替代Servlet,请求映射一目了然
  5. 配置太多 → SpringBoot自动配置,开箱即用

理解了这个进化过程,你就不会觉得框架是"魔法"了。每个框架都是在前人的基础上解决具体问题,本质上做的事情都一样:封装重复代码、提供更好的抽象、让开发者专注于业务逻辑。

现在回头看我带的那个新人写的JDBC代码,一点都不意外。每个人都是从最基础的东西学起的。但当你理解了框架解决了什么问题、为什么这么设计,用起来就会得心应手,而不是死记硬背配置。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐