网站内链seo,100种禁用的视频软件短视频,安卓app下载安装,南京网络科技公司有哪些目录 1. Spring Boot单元测试
1.1 什么是单元测试?
1.2 单元测试有哪些好处?
1.3 Spring Boot 单元测试使用
单元测试的实现步骤
1. 生成单元测试类
2. 添加单元测试代码
简单的断言说明
2. Mybatis 单表增删改查
2.1 单表查询
2.2 参数占位符 ${} 和 #{}
${} 和 …目录 1. Spring Boot单元测试
1.1 什么是单元测试?
1.2 单元测试有哪些好处?
1.3 Spring Boot 单元测试使用
单元测试的实现步骤
1. 生成单元测试类
2. 添加单元测试代码
简单的断言说明
2. Mybatis 单表增删改查
2.1 单表查询
2.2 参数占位符 ${} 和 #{}
${} 和 #{}的区别
1. 作用不同
2. 安全性: ${} 的SQL注入问题
${} 应用场景
2.3 单表修改操作
2.4 单表删除操作
2.5 单表添加操作
添加返回影响行数
添加返回影响行数和id
2.6 like查询
2.7 标签返回类型使用背景使用
1. Spring Boot单元测试
1.1 什么是单元测试?
单元测试(unit testing)是指对软件中的最小可测试单元进行检查和验证的过程就叫单元测试。
单元测试是开发者编写的一小段代码用于检验被测代码的一个很小的、很明确的(代码)功能是否正确。执行单元测试就是为了证明某段代码的执行结果是否符合我们的预期。如果测试结果符合我们的预期称之为测试通过否则就是测试未通过 (或者叫测试失败)
1.2 单元测试有哪些好处?
可以非常简单、直观、快速的测试某一个功能是否正确。使用单元测试可以帮我们在打包的时候发现一些问题因为在打包之前所有的单元测试必须通过, 否则不能打包成功。 使用单元测试在测试功能的时候可以不污染连接的数据库也就是可以不对数据库进行任何改变的情况下测试功能。
1.3 Spring Boot 单元测试使用
Spring Boot 项目创建时会默认单元测试框架 spring-boot-starter-test而这个单元测试框架主要是依靠另个著名的测试框架 JUnit 实现的打开 pom.xml 就可以看到以下信息是 Spring Boot 项目创建是自动添加的: dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency
单元测试的实现步骤
1. 生成单元测试类 最终生成的代码:
package com.example.demo.mapper;import org.junit.jupiter.api.Test;class UserMapperTest {Testvoid getAll() {}
}这个时候此方法是不能调用到任何单元测试的方法的此类只生成了单元测试的框架类具体的业务代码要自己填充。
2. 添加单元测试代码
在测试类上添加Spring Boot 框架测试注解: SpringBootTest
import org.springframework.boot.test.context.SpringBootTest;SpringBootTest // 表示当前单元测试的类是运行在 Spring Boot 环境中的(一定不能省略)
class UserMapperTest {// ..
}
添加单元测试业务逻辑 Autowiredprivate UserMapper userMapper;Testvoid getAll() {ListUserEntity list userMapper.getAll();System.out.println(list.size());}
简单的断言说明 方法 说明 assertEquals 判断两个对象或两个原始类型是否相等 assertNotEquals 判断两个对象或两个原始类型是否不相等 assertSame 判断两个对象引用是否指向同一个对象 assertNotSame 判断两个对象引用是否指向不同的对象 assertTrue 判断给定的布尔值是否为 true assertFalse 判断给定的布尔值是否为 false assertNull 判断给定的对象引用是否为 null assertNotNull 判断给定的对象引用是否不为 null
断言: 如果断言失败则后面的代码都不会执行. 2. Mybatis 单表增删改查
2.1 单表查询
下面我们来实现一下根据用户id查询用户信息的功能.
在UserMapper类中添加接口:
// 根据 id 查询用户对象
UserEntity getUserById(Param(uid) Integer id); // Param是给形参起名 select idgetUserById resultTypecom.example.demo.entity.UserEntityselect * from userinfo where id${uid}/select
注: 上面 ${uid} 中的uid对应Param的uid
使用单元测试的方式去调用它. Testvoid getUserById() {UserEntity user userMapper.getUserById(2);System.out.println(user);}
那么我们的预期结果是能够打印出数据库中zhangsan的数据: 执行结果: 可以看到, 预期结果成功执行了. 2.2 参数占位符 ${} 和 #{}
Mybatis获取动态参数有两种实现:
${paramName} - 直接替换#{paramName} - 占位符模式 验证直接替换:
在Spring配置文件中有一个配置, 只需要把这个配置给它配置之后, 那么Mybatis的执行SQL(Mybatis底层是基于JDBC), 最终会生成JDBC的执行SQL和它的执行模式, 那么我们就可以把这个执行的SQL语句打印出来.
需要配置两个配置项, 一个是日志打印的实现, 另一个是设置日志打印的级别 (SQL的打印默认输出的级别的debug级别, 但日志默认级别的info, 默认info要大于debug, 所以并不会显示, 所以要去进行日志级别的设置).
# 打印 Mybatis 执行 SQL
mybatis.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl
logging.level.com.example.demodebug
配置完成之后再次运行刚才的测试代码, 可以看到SQL的相关信息都被打印了出来, 所以可以知道$是直接替换的模式. 将上文的 $ 换成 # , 会看到的是, SQL语句的id变成了?, 也就是变成了占位符的模式. 而占位符的模式是预执行的, 而预执行是比较安全的, 具体来说预执行可以有效的排除SQL注入的问题. ${} 和 #{}的区别
1. 作用不同
${} 所见即所得, 直接替换, #{} 是预处理的.
在进行使用的时候, 如果传的是int这种简单数据类型的时候, 两者是没有区别的, 但是如果更复杂点的使用varchar, 就会有安全的问题出现.
在UserMapper类中添加接口:
// 根据名称查询用户对象
UserEntity getUserByUserName(Param(username) String username); select idgetUserByUserName resultTypecom.example.demo.entity.UserEntityselect * from userinfo where username#{username}/select
测试: Testvoid getUserByUserName() {UserEntity user userMapper.getUserByUserName(zhangsan);System.out.println(user);} 测试结果没有问题, 那么再将#换成$. select idgetUserByUserName resultTypecom.example.demo.entity.UserEntityselect * from userinfo where username${username}/select 这时程序报错没有找到zhangsan, 并且我们看到SQL语句变成了.
在数据库客户端中执行图中SQL语句也是会报出和上图一样的错. 那么这里的原因就在于刚才我们的代码中, ${}是直接替换的模式, 当加上单引号后再次运行就正常运行了. select idgetUserByUserName resultTypecom.example.demo.entity.UserEntityselect * from userinfo where username${username}/select 但是加单引号只能保证不报错, 但是不能保证安全性问题.
所以当我们遇到是int类型的时候, ${} 和 #{} 在执行上没有什么区别, 当出现字符型的时候${} 就有可能会出现问题.
2. 安全性: ${} 的SQL注入问题
${} 的安全性问题出现在登录, 接下来我们以登录为例看一下什么是SQL注入.
首先SQL注入是 用户用了并不是一个真实的用户名和密码, 但是却查询到了数据. 我们通过代码说明.
// 登录方法
UserEntity login(UserEntity user); select idlogin resultTypecom.example.demo.entity.UserEntityselect * from userinfo where username${username} and password${password}/select
注: 当Interface传的是对象时, xml中获取属性时, 也就是{}里面直接写对象的属性名即可, 无需对象.属性, 这是Mybatis的约定 为了演示效果, 我们在数据库中删掉id2的zhangsan. 先来看正常的用户行为. Testvoid login() {String username admin;String password admin;UserEntity inputUser new UserEntity();inputUser.setUsername(username);inputUser.setPassword(password);UserEntity user userMapper.login(inputUser);System.out.println(user);} 可以看到, 找到了相关信息.
当输入错误密码时, 即:
String password admin2; 可以看到, 结果是null, 以上都是正常的行为.
接下来我们来看一个特殊的, 不正常的行为, 输入如下密码:
String password or 11; 此时我们可以发现, 输入了一个不正常的密码, 却把admin查出来了, 这就是SQL注入, 对于程序来说是非常危险的.
那么我们可以看到这里的SQL语句是
select * from userinfo where usernameadmin and password or 11
所以这便是这里出错的原因, 它把字符串误解析成SQL指令去执行了, 使逻辑运行结果与预期不同, 但却正常执行.
当把 ${} 改为 #{} 后, 再次测试, 可以看到结果是null. 由上可见, 使用 ${} 是会安全性问题的, 而使用 #{} 就不会出现安全性问题, 原因在于 #{} 使用了JDBC的占位符的模式, 那么这种模式是预执行的, 是直接当成字符串来执行的. ${} 应用场景
${} 虽然在查询的时候会有安全性问题, 但是它也有具体的应用场景, 比如以下场景:
在淘宝中有时候需要按照某种属性进行排序, 比如价格低到高或者高到低, 这时SQL传递的就是order by后的规则asc或desc. 使用 ${sort} 可以实现排序查询而使用 #{sort} 就不能实现排序查询了因为当使用 #{sort} 查询时如果传递的值为 String 则会加单引号就会导致 sql 错误。 那么对于我们之前的程序, 我们也可以进行类似的应用. ListUserEntity getAllByIdOrder(Param(ord) String order); select idgetAllByIdOrder resultTypecom.example.demo.entity.UserEntityselect * from userinfo order by id ${ord}/select Testvoid getAllByIdOrder() {ListUserEntity list userMapper.getAllByIdOrder(desc);System.out.println(list.size());} 这时使用 #{} 就会报错了. select idgetAllByIdOrder resultTypecom.example.demo.entity.UserEntityselect * from userinfo order by id #{ord}/select 既然 ${} 有用, 但是它也极其的危险, 在使用的时候要注意, 要保证它的值必须得被枚举. 所以尽量少用. 2.3 单表修改操作
比如需要修改用户密码.
首先, 在Interface声明方法, // 修改密码int updatePassword(Param(id) Integer id,Param(password) String password,Param(newPassword) String newPassword);
然后在xml中实现方法, 注意修改操作是使用update标签. update idupdatePasswordupdate userinfo set password#{newPassword}where id#{id} and password#{password}/update Testvoid updatePassword() {int result userMapper.updatePassword(1, admin, 123456);System.out.println(修改: result);} 运行前后查询数据库, 可以看到, password已经成功修改了.
当再次修改newPassword参数的代码时, 即:
int result userMapper.updatePassword(1, admin, 666666); 这里说明, 注入参数有问题, 代码没问题.
不过, 这里的测试是把原本数据库污染了, 违背了单元测试的初衷, 那么要想不污染数据库, 需要在测试类前加上Transactional事务注解. Transactional // 事务Testvoid updatePassword() {int result userMapper.updatePassword(1, 123456, 666666);System.out.println(修改: result);}
当加上注解之后, 测试的代码可以正常执行, 但是就不会污染数据库了. 看到打印了修改: 1, 就说明成功修改了.
在代码执行的时候不会进行干扰的, 只不过在执行之初, 会开启一个事务, 等全部代码执行完了, 比如这里的修改: x已经正常打印了, 然后在它执行完会进行rollback回滚操作, 所以就不会污染数据库了. 验证数据库是否污染: 2.4 单表删除操作
// 删除用户
int delById(Param(id) Integer id); delete iddelByIddelete from userinfo where id#{id}/delete TransactionalTestvoid delById() {int id 1;int result userMapper.delById(id);System.out.println(删除结果: result);} 2.5 单表添加操作
添加返回影响行数
// 添加用户
int addUser(UserEntity user); insert idaddUserinsert into userinfo(username,password) values(#{username},#{password})/insert Testvoid addUser() {UserEntity user new UserEntity();user.setUsername(lisi);user.setPassword(123456);int result userMapper.addUser(user);System.out.println(添加: result);} 添加返回影响行数和id
int addUserGetId(UserEntity user); insert idaddUserGetId useGeneratedKeystrue keyPropertyidinsert into userinfo(username,password) values(#{username},#{password})/insert Testvoid addUserGetId() {UserEntity user new UserEntity();user.setUsername(lili);user.setPassword(123456);int result userMapper.addUserGetId(user);System.out.println(添加结果: result);System.out.println(ID: user.getId());} 2.6 like查询
like使用 #{} 会报错 // 根据用户名模糊查询ListUserEntity getListByName(Param(username) String username); select idgetListByName resultTypecom.example.demo.entity.UserEntityselect * from userinfo where username like %#{username}%/select Testvoid getListByName() {String username zhang;ListUserEntity list userMapper.getListByName(username);list.stream().forEach(System.out::println);} 可以看到, 当我们使用#{}的方式进行like查询的时候, 它是有问题的, 因为它会把#{username}作为占位符, 看到传过来的参数是String后, 相当于又加了一个单引号, 也就是%zhang%, 所以它是报错的.
在数据库中演示一下. 那么可以通过MySQL内置方法concat解决这个问题, concat能实现字符的拼接. select idgetListByName resultTypecom.example.demo.entity.UserEntityselect * from userinfo where username like concat(%,#{username},%)/select 可以看到, 我们可以查到zhangsan的信息了, 说明此时查询是没有问题的. 2.7 select标签返回类型
我们知道, select查询标签至少需要设置两个属性:
id属性: 用于标识实现接口中的哪个方法;
结果映射(即返回)属性: 结果映射有两种实现标签: resultType和resultMap
resultType
绝大多数场景可以使用resultType返回, 如下所示: select idgetAll resultTypecom.example.demo.entity.UserEntityselect * from userinfo/select
它的优点是使用方便直接定义到某个实体类即可.
resultMap
resultMap使用场景:
字段名称和程序中的属性名不同的情况可使用 resultMap 配置映射一对一和一对多关系可以使用 resultMap 映射并查询数据 使用背景
我们以数据库中的字段为例, 比如当项目实体类中密码的属性为pwd, 而数据库中对应的字段为password, 这个时候Mybatis就匹配不到了, 原本实体类中为password, Mybatis因为做了映射, 而映射规则是根据名称进行匹配, 所以能够拿到数据库中的信息(Mybatis是一个ORM框架). 那么问题在哪里呢?
我们来看前文的getListByName(), 原来使用zhang把信息查询到了, 但是我们此时将实体类中的password改为pwd, 再次测试, 我们可以会发现, 执行没有报错, JDBC可以正常拿到password, 但是在映射的时候给pwd没有映射上.
这里就是因为对比的时候pwd和password是不相同的, Mybatis在属性中没有找到pwd.
所以这时我们resultType使用实体类已经和数据库的表对应不上了, 那么就使用resultMap. 使用 resultMap idBaseMap typecom.example.demo.entity.UserEntityid propertyid columnid/idresult propertyusername columnusername/resultresult propertypwd columnpassword/resultresult propertycreatetime columncreatetime/resultresult propertyupdatetime columnupdatetime/result/resultMapselect idgetListByName resultMapBaseMapselect * from userinfo where username like concat(%,#{username},%)/select 可以看到, 此时pwd是有值的, 数据库中为password, 程序中是pwd, 依然能够拿到值. 注: 如果依然要用resultType就需要对SQL语句修改. select idgetListByName resultTypecom.example.demo.entity.UserEntityselect id,username,password as pwd from userinfo where username like concat(%,#{username},%)/select 可以看到成功的拿到了password, 并且在SQL中是实现了重命名.