【Spring Boot】深入解析:#{} 和 ${}

1.#{} 和 ${}的使用

1.1数据准备

1.1.1.MySQL数据准备

(1)创建数据库:

sql 复制代码
CREATE DATABASE mybatis_study DEFAULT CHARACTER SET utf8mb4;

(2)使用数据库

sql 复制代码
-- 使⽤数据数据
USE mybatis_study;

(3)创建用户表

sql 复制代码
-- 创建表[⽤⼾表]

CREATE TABLE `user_info` (
 `id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
 `username` VARCHAR ( 127 ) NOT NULL,
 `password` VARCHAR ( 127 ) NOT NULL,
 `age` TINYINT ( 4 ) NOT NULL,
 `gender` TINYINT ( 4 ) DEFAULT '0' COMMENT '1-男 2-⼥ 0-默认',
 `phone` VARCHAR ( 15 ) DEFAULT NULL,
 `delete_flag` TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
 `create_time` DATETIME DEFAULT now(),
 `update_time` DATETIME DEFAULT now() ON UPDATE now(),
 PRIMARY KEY ( `id` ) 
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4; 

(4)添加用户信息

sql 复制代码
-- 添加⽤⼾信息
INSERT INTO mybatis_study.user_info( username, `password`, age, gender, phone )
VALUES ( 'admin', 'admin', 18, 1, '18612340001' );
INSERT INTO mybatis_study.user_info( username, `password`, age, gender, phone )
VALUES ( 'zhangsan', 'zhangsan', 18, 1, '18612340002' );
INSERT INTO mybatis_study.user_info( username, `password`, age, gender, phone )
VALUES ( 'lisi', 'lisi', 18, 1, '18612340003' );
INSERT INTO mybatis_study.user_info( username, `password`, age, gender, phone )
VALUES ( 'wangwu', 'wangwu', 18, 1, '18612340004' );

1.1.2.创建对应的实体类

实体类的属性名与表中的字段名⼀⼀对应

java 复制代码
@Data
public class UserInfo {

    private Integer id;
    private String username;
    private String password;
    private Integer age;
    private Integer gender;
    private String phone;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;

}

注意:在实际开发中不管什么实体类都要设置删除标志、创建时间、修改时间

1.2 获取Integer类型

1.2.1 #{}

Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {
    // 获取参数中的 UserId
    @Select("select * from user_info where id = #{userId} ")
    UserInfo queryById(@Param("userId") Integer id);

测试代码:

java 复制代码
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {

    @Test
    void queryById() {
        UserInfo result = userInfoMapper.queryById(8);
        log.info(result.toString());
    }
}

运行结果:

通过日志可以发现,进行占位,传的参数进行绑定到占位符。

1.2.2 ${}

Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {
    // 获取参数中的 UserId
    @Select("select * from user_info where id = ${userId} ")
    UserInfo queryById(@Param("userId") Integer id);

测试代码:

java 复制代码
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {

    @Test
    void queryById() {
        UserInfo result = userInfoMapper.queryById(8);
        log.info(result.toString());
    }
}

运行结果:

通过日志可以发现,SQL命令是完整的,因为,该方法是把字符串拼接在一起执行的。

1.3 获取String类型

1.3.1 #{}

Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {
    // 获取参数中的 username
    @Select("select * from user_info where username = #{username} ")
    List<UserInfo> queryByUsername( String username);

测试代码:

java 复制代码
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {
    @Autowired
    private UserInfoMapper userMapper;

    @Test
    void queryByUsername() {
        userMapper.queryByUsername("lisi");
    }
}

运行结果:

通过日志可以发现,进行占位,传的参数进行绑定到占位符。

1.3.2 ${}

Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {
    // 获取参数中的 username
    @Select("select * from user_info where username = ${username} ")
    List<UserInfo> queryByUsername( String username);

测试代码:

java 复制代码
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {
    @Autowired
    private UserInfoMapper userMapper;

    @Test
    void queryByUsername() {
        userMapper.queryByUsername("lisi");
    }
}

运行结果:报错

从SQL语句中明显的看到WHERE username 后面的字符串没有引号,导致报错。

因为,${}直接把字符内容直接放进SQL语句中而没有加单引号。

修改后的mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {
    // 获取参数中的 username
    @Select("select * from user_info where username = '${username}' ")
    UserInfo queryByUsername( String username);

2.#{} 和 ${}的区别

2.1 预编译SQL和即时SQL的执行过程

2.1.1 预编译SQL执行过程

#{}是预编译SQL。

第一步:数据库客户端(如 JDBC 驱动)将 SQL 模板发送到数据库服务器。

java 复制代码
// SQL模版
PreparedStatement pstmt = connection.prepareStatement("SELECT * FROM user WHERE id = ? AND name = ?");

第二步 :SQL 预编译

(1)数据库解析 SQL 模板,生成执行计划(包括语法检查、语义分析、优化等),并缓存该计划。

(2)此时,占位符 ? 的具体值尚未填充,数据库只处理 SQL 的结构。

第三步:客户端通过 PreparedStatement 的方法设置参数值

java 复制代码
//参数值以二进制形式单独发送到数据库,不会直接拼接到 SQL 中,避免了 SQL 注入
pstmt.setInt(1, 123);    // 绑定 id
pstmt.setString(2, "Alice"); // 绑定 name

第四步 :SQL 执行

(1)数据库使用缓存的执行计划,将绑定参数代入执行计划,直接运行查询或更新操作。

(2)如果相同的 SQL 模板再次执行(仅参数不同),数据库可复用缓存的执行计划,减少编译开销。

2.1.2 即时QL执行过程

${}是即时SQL。

第一步:SQL 语句拼接

sql 复制代码
SELECT * FROM user ORDER BY ${columnName}

//如果 columnName = "age"

//生成
SELECT * FROM user ORDER BY age; DROP TABLE user;

第二步 :SQL 发送到数据库

客户端将拼接好的完整 SQL 字符串通过 Statement 或类似接口发送到数据库

java 复制代码
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);

第三步 :SQL解析与编译

语法解析:检查 SQL 语句的语法是否正确。

语义分析:验证表名、列名等是否存在,权限是否足够。

优化:生成执行计划,选择最优的查询路径。

第四步 :SQL执行

数据库根据生成的执行计划执行 SQL,完成查询或更新操作。

2.2性能比较

预编译SQL(#{})性能更高:

绝⼤多数情况下, 某⼀条 SQL 语句可能会被反复调⽤执⾏, 或者每次执⾏的时候只有个别的值不同(⽐如 select 的 where ⼦句值不同, update 的 set ⼦句值不同, insert 的 values 值不同). 如果每次都需要经过上⾯的语法解析, SQL优化、SQL编译等,则效率就明显不⾏了

预编译SQL,编译⼀次之后会将编译后的SQL语句缓存起来,后⾯再次执⾏这条语句时,不会再次编译 (只是输⼊的参数不同), 省去了解析优化等过程, 以此来提⾼效率

预编译SQL(#{})更安全(防⽌SQL注⼊):

由于没有对⽤⼾输⼊进⾏充分检查,⽽SQL⼜是拼接⽽成,在⽤⼾输⼊参数时,在参数中添加⼀些 SQL关键字,达到改变SQL运⾏结果的⽬的,也可以完成恶意攻击。

2.3 排序举例

排序需要用到SQL的关键字ascdesc,把该两个关键字设置为参数时需要用到${},因为#{}会把ascdesc认为是字符串

2.3.1 #{}

Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {

    @Select("select * from userInfo order by username #{flag}")
    List<UserInfo> findAll(String flag);
}

测试代码

java 复制代码
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {

    @Autowired
    private UserInfoMapper userMapper;

    @Test
    void findAll() {
        userMapper.findAll("asc");
    }
}

运行结果:

2.3.2 #{}

Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {

    @Select("select * from userInfo order by username ${flag}")
    List<UserInfo> findAll(String flag);
}

测试代码

java 复制代码
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {

    @Autowired
    private UserInfoMapper userMapper;

    @Test
    void findAll() {
        userMapper.findAll("asc");
    }
}

运行结果:

2.4 like 查询

2.4.1 #{}

Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {

    @Select("select * from user_info where username like '%#{s}%'")
    List<UserInfo> queryLike(String s);
}

测试代码:

java 复制代码
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {

    @Autowired
    private UserInfoMapper userMapper;

    @Test
    void queryLike() {
        String s = "6";
        userMapper.queryLike(s);
    }
}

运行结果:

把 #{} 改成 可以正确查出来 , 但是 {} 可以正确查出来, 但是 可以正确查出来,但是{}存在SQL注⼊的问题, 所以不能直接使⽤ ${}.解决办法: 使⽤ mysql 的内置函数 concat() 来处理,实现代码如下:

修改后的Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {

    @Select("select * from user_info where username like concat('%',#{s},'%') ")
    List<UserInfo> queryLike(String s);

运行结果:

2.4.2 ${}

Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {

    @Select("select * from user_info where username like '%${s}%' ")
    List<UserInfo> queryLike(String s);
}

测试代码:

java 复制代码
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {

    @Autowired
    private UserInfoMapper userMapper;

    @Test
    void queryLike() {
        String s = "6";
        userMapper.queryLike(s);
    }
}

运行结果:

3.什么是SQL注入?

SQL注⼊:是通过操作输⼊的数据来修改事先定义好的SQL语句,以达到执⾏代码对服务器进⾏攻击的⽅法。

举例:

下面定义的接口是由username得到该username的信息

Mapper接口:

java 复制代码
@Mapper
public interface UserInfoMapper {
    // 获取参数中的 username
    @Select("select * from user_info where username = ${username} ")
    List<UserInfo> queryByUsername( String username);

可以通过输入' or username='来获取该表中所有人的信息

测试代码:

java 复制代码
@Slf4j
@SpringBootTest //启动Sring 容器
class UserInfoMapperTest {
    @Autowired
    private UserInfoMapper userMapper;

    @Test
    void queryByUsername() {
        userMapper.queryByUsername("lisi");
    }
}

运行结果:

可以看出来, 查询的数据越界了接口的定义。所以⽤于查询的字段,尽量使⽤#{}预查询的⽅式

SQL注⼊是⼀种⾮常常⻅的数据库攻击⼿段, SQL注⼊漏洞也是⽹络世界中最普遍的漏洞之⼀。

4.数据库连接池

4.1介绍

数据库连接池负责分配、管理和释放数据库连接,它允许应⽤程序重复使⽤⼀个现有的数据库连接,⽽不是再重新建⽴⼀个.

没有使⽤数据库连接池的情况: 每次执⾏SQL语句, 要先创建⼀个新的连接对象, 然后执⾏SQL语句, SQL语句执⾏完, 再关闭连接对象释放资源. 这种重复的创建连接, 销毁连接⽐较消耗资源

使⽤数据库连接池的情况: 程序启动时, 会在数据库连接池中创建⼀定数量的Connection对象, 当客⼾请求数据库连接池, 会从数据库连接池中获取Connection对象, 然后执⾏SQL, SQL语句执⾏完, 再把 Connection归还给连接池.

优点:

1.减少了⽹络开销

2.资源重⽤

3.提升了系统的性能

4.26.2使⽤

常⻅的数据库连接池:

•C3P0

•DBCP

•Druid

•Hikari

⽬前⽐较流⾏的是 Hikari, Druid

Hikari : SpringBoot默认使⽤的数据库连接池

Hikari 是⽇语"光"的意思(ひかり), Hikari也是以追求性能极致为⽬标

2.Druid

如果我们想把默认的数据库连接池切换为Druid数据库连接池, 只需要引⼊相关依赖即可

xml 复制代码
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid-spring-boot-3-starter</artifactId>
	<version>1.2.21</version>
</dependency>

如果SpringBoot版本为2.X, 使⽤druid-spring-boot-starter 依赖

xml 复制代码
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid-spring-boot-starter</artifactId>
	<version>1.1.17</version>
</dependency>

Druid连接池是阿⾥巴巴开源的数据库连接池项⽬

功能强⼤,性能优秀,是Java语⾔最好的数据库连接池之⼀

相关推荐
Barcke7 分钟前
深入浅出 Spring WebFlux:从核心原理到深度实战
后端
JuiceFS7 分钟前
从 MLPerf Storage v2.0 看 AI 训练中的存储性能与扩展能力
运维·后端
大鸡腿同学9 分钟前
Think with a farmer's mindset
后端
Moonbit30 分钟前
用MoonBit开发一个C编译器
后端·编程语言·编译器
Reboot1 小时前
达梦数据库GROUP BY报错解决方法
后端
稻草人22221 小时前
java Excel 导出 ,如何实现八倍效率优化,以及代码分层,方法封装
后端·架构
渣哥1 小时前
原来 Java 里线程安全集合有这么多种
java
间彧1 小时前
Spring Boot集成Spring Security完整指南
java
掘金者阿豪2 小时前
打通KingbaseES与MyBatis:一篇详尽的Java数据持久化实践指南
前端·后端
间彧2 小时前
Spring Secutiy基本原理及工作流程
java