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

相关推荐
苍煜39 分钟前
Maven构建流程详解:如何正确管理微服务间的依赖关系-当依赖的模块更新后,我应该如何重新构建主项目
java·微服务·maven
冼紫菜42 分钟前
[特殊字符]CentOS 7.6 安装 JDK 11(适配国内服务器环境)
java·linux·服务器·后端·centos
isyangli_blog1 小时前
(1-4)Java Object类、Final、注解、设计模式、抽象类、接口、内部类
java·开发语言
秋野酱2 小时前
Spring Boot 项目的计算机专业论文参考文献
java·spring boot·后端
码视野2 小时前
基于Spring Boot和Vue的在线考试系统架构设计与实现(源码+论文+部署讲解等)
vue.js·spring boot·系统架构
士别三日&&当刮目相看2 小时前
数据结构*优先级队列(堆)
java·数据结构
香饽饽~、2 小时前
【第二篇】 初步解析Spring Boot
java·spring boot·后端
坎布里奇2 小时前
java -jar命令运行 jar包时如何运行外部依赖jar包
java·pycharm·jar
冷yan~3 小时前
GitHub文档加载器设计与实现
java·人工智能·spring·ai·github·ai编程