从JDBC到SpringBoot:Java后端开发的框架进化之路
从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&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);
}
}
这段代码有什么问题?UserService和UserDaoImpl强耦合了。如果有一天要换成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 | 自动配置、约定大于配置、快速启动 | 几乎没有 |
每一次进化都是为了解决前一阶段的痛点:
- JDBC太啰嗦 → MyBatis帮你封装,SQL写在XML里,结果集自动映射
- 对象管理太乱 → Spring帮你创建和管理对象,依赖自动注入
- 通用逻辑太散 → Spring AOP帮你把日志、事务等横切逻辑集中管理
- Web开发太繁琐 → SpringMVC用注解替代Servlet,请求映射一目了然
- 配置太多 → SpringBoot自动配置,开箱即用
理解了这个进化过程,你就不会觉得框架是"魔法"了。每个框架都是在前人的基础上解决具体问题,本质上做的事情都一样:封装重复代码、提供更好的抽象、让开发者专注于业务逻辑。
现在回头看我带的那个新人写的JDBC代码,一点都不意外。每个人都是从最基础的东西学起的。但当你理解了框架解决了什么问题、为什么这么设计,用起来就会得心应手,而不是死记硬背配置。
更多推荐



所有评论(0)