【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语⾔最好的数据库连接池之⼀

相关推荐
帆仔哟20 分钟前
【农气项目】基于适宜度的产量预报
java
MerlinTheMagic32 分钟前
Spring AI 核心概念
java·人工智能·spring
冼紫菜1 小时前
基于Redis实现高并发抢券系统的数据同步方案详解
java·数据库·redis·后端·mysql·缓存·性能优化
酷ku的森1 小时前
Spring Web MVC入门
spring·mvc
碎梦归途1 小时前
23种设计模式-结构型模式之适配器模式(Java版本)
java·开发语言·jvm·单例模式·设计模式·适配器模式
wkj0011 小时前
JDK版本与Spring Boot版本之间对应关系
java·linux·spring boot
异常君1 小时前
HTTP头中的Accept-Encoding与Content-Encoding深度剖析
后端·nginx·http
西洼工作室1 小时前
黑马商城-微服务笔记
java·笔记·spring·微服务
异常君1 小时前
MySQL重复数据克星:7种高效处理方案全解析
java·后端·mysql
异常君1 小时前
Spring 定时任务执行一次后不再触发?5 大原因与解决方案全解析
java·后端·spring