JavaEE 常用框架
SpringSecerity
springsecurity 是用户请求进来,判断有没有请求的权限,抛出异常,重定向跳转。
它的底层是一个过滤器链。
它自带登录页和退出页,下面的配置中,我设置了默认登录页用自定义的登录页。
首先导入依赖jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
无需在配置文件中配置内容。
紧接着自定义一个配置类就可以了:
@EnableWebSecurity //开启WebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
//首页所有人可以访问,但是功能页只有对应有权限的人才能访问
// 请求授权的规则
http.authorizeRequests()
//"/afei" 此路径下所有均可访问
.antMatchers("/afei").permitAll()
// "/leve1/**" 此路径下只有表明为vip1的权限才可以访问
.antMatchers("/leve1/**").hasRole("vip1")
// "/leve2/**" 此路径下只有表明为vip2的权限才可以访问
.antMatchers("/leve2/**").hasRole("vip2");
/** http.formLogin() 没有权限 默认回到登录页 //开启登录页面
* 定制登录页为自己的登录页 .loginPage("/loginPage")
* .usernameParameter("user").passwordParameter("pwd") 设置接收前端表单的name值
**/ loginProcessingUrl("/login"); 表单提交路径
http.formLogin().loginPage("/loginPage").usernameParameter("user").passwordParameter("pwd").loginProcessingUrl("/login");
//防止网站工具 get 是明文不安全会被拦截,
http.csrf().disable(); //关闭防止跨战请求的拦截
//开启注销
// .logoutSuccessUrl("/afei"); 注销成功返回 /afei页面
http.logout().logoutSuccessUrl("/afei");
//开启记住我功能 其实就是个Cookie 默认保存两周
//rememberMeParameter("remember"); 接受前端的remember信息
http.rememberMe().rememberMeParameter("remember");
}
//认证 SpringBoot 2.1.x 可以直接使用
//但是高版本会报错,报错的信息为密码未加密,密码为明文形式是不安全的
// 密码未编码 PasswordEncoder
//Spring Security 5.0 提供了很多加密方式
/*@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication() .withUser("admin").password("admin").roles("vip1")
.and().withUser("afei").password("afei").roles("vip2")
.and().withUser("root").password("root").roles("vip1","vip2");
}*/
//使用MD5对密码进行编译
//.withUser("admin") 配置账号
//.password(new BCryptPasswordEncoder().encode("admin")) 配置密码 并MD5加密
// .roles("vip1") 给此账号分配权限
// .and() 是拼接的意思,就是继续拼接信息
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password(new BCryptPasswordEncoder().encode("admin")).roles("vip1")
.and().withUser("afei").password(new BCryptPasswordEncoder().encode("afei")).roles("vip2")
.and().withUser("root").password(new BCryptPasswordEncoder().encode("root")).roles("vip1","vip2");
}}
出现的样子用语言描述不清,cv运行一波就理解了~
现在展示页面,可以cv运行一波就行
主页html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
Test
<a href="/leve1/1">vip1</a>
<a href="/leve2/2">vip2</a>
<a href="/logout">注销</a>
</body>
</html>
vip1:页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
vip1
</body>
</html>
vip2:页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
vip2
</body>
</html>
自定义登录页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post" action="/login">
<input type="text" name="user"><br>
<input type="text" name="pwd"><br>
<input type="checkbox" name="remember">记住我<br>
<button type="submit">登录</button>
</form>
</body>
</html>
controller层:
@Controller
public class AllController {
@RequestMapping("/leve1/{id}")
public String leve1(@PathVariable("id") int id){
return "/leve"+id+".html";
}
@RequestMapping("/leve2/{id}")
public String leve2(@PathVariable("id") int id){
return "/leve"+id+".html";
}
@RequestMapping("/loginPage")
public String loginPage(){
return "login.html";
}
}
MybatisPlus
1、什么是MybatisPlus
MyBatis-Plus是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
- 无侵入
- 损耗小:启动即会自动注入基本 CURD
- 支持 Lambda 形式调用
- 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence)
- 支持 ActiveRecord 模式
- 支持自定义全局通用操作
- 内置代码生成器
- 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
- 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
- 内置全局拦截插件
2、快速入门
创建数据库mybatis_plus
DROP TABLE IF EXISTS user;
CREATE TABLE user
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
DELETE FROM user;
INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');
初始化项目
引用spring boot starter 父工程
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.5</version>
<relativePath/>
</parent>
引入spring-boot-starter、spring-boot-starter-test、mybatis-plus-boot-starter、h2依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
注意:尽量不要同时导入mybatis和mybatis_plus,版本差异
application.yml
# DataSource Config
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql:///mybatis_plus?userUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
在spring boot启动类中添加@MapperScan注解,扫描Mapper文件夹:
@SpringBootApplication
@MapperScan("com.wen.mybatis_plus.mapper") //扫描mapper
public class MybatisPlusApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisPlusApplication.class, args);
}
}
3、配置日志
所有的SQL都是不可见的,所以在后台是希望看到SQL是怎么执行的,就必须要配置日志。
在.yml配置文件中配置日志:
#配置日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
简单使用
int insert = userMapper.insert(user);//如果没有设置id,那么会自动生成id
System.out.println(insert);//受影响行数
System.out.println(user);//id会自动回填
User user = new User();
//可以通过条件自动拼接动态SQL
user.setId(5L);
user.setName("id:5,修改过后");
//updateById 参数是一个对象!
int i = userMapper.updateById(user);
System.out.println(i);
//注意:updateById 参数是一个对象!而不是ID
4、ID生成器
在复杂分布式系统中,往往需要大量的数据和消息进行唯一标识。比如支付宝每一个账号在数据库分表后都需要有一个唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。
所以,生成的ID需要具备一下特点:
- 全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
- 趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
- 单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
- 信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。
上述123对应三类不同的场景,3和4需求还是互斥的,无法使用同一个方案满足。
在这里只讲两种自动生成ID的方案UUID和SnowFlake
在用户ID上添加@TableId注解,里面的值便是使用主键自动生成的方法
@Data
public class User {
//自 3.3.0 开始,默认使用雪花算法+UUID(不含中划线)
//对应数据库中的主键(UUID、自增id、雪花算法、redis、zookeeper)
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer age;
private String email;
}
点击 IdType查看源码看有哪些自动生成方法
/**
* 生成ID类型枚举类
*
* @author hubin
* @since 2015-11-10
*/
@Getter
public enum IdType {
/**
* 数据库ID自增
* <p>该类型请确保数据库设置了 ID自增 否则无效</p>
*/
AUTO(0),
/**
* 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
*/
NONE(1),
/**
* 用户输入ID
* <p>该类型可以通过自己注册自动填充插件进行填充</p>
*/
INPUT(2),
/* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
/**
* 分配ID (主键类型为number或string),
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)
*
* @since 3.3.0
*/
ASSIGN_ID(3),
/**
* 分配UUID (主键类型为 string)
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(UUID.replace("-",""))
*/
ASSIGN_UUID(4);
private final int key;
IdType(int key) {
this.key = key;
}
}
UUID
UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:550e8400-e29b-41d4-a716-446655440000
优点:
- 性能非常高:本地生成,没有网络消耗。
缺点:
- 没有排序,无法保证趋势递增。
- UUID往往使用字符串存储,查询的效率比较低。
- 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
- 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
- ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用:
- MySQL官方有明确的建议主键要尽量越短越好[4],36个字符长度的UUID不符合要求。
- 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。
SnowFlake(雪花算法)
这种方案大致来说是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等
核心思想:
使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生4096个ID),最后还有一个符号位,永远是0,。
优点:
- 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
- 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
- 可以根据自身业务特性分配bit位,非常灵活。
缺点:
- 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
5、乐观锁和悲观锁
悲观锁:十分悲观,认为总是出现问题,无论干什么都会上锁,再去操作
悲观锁是基于一种悲观的态度类来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的;
特点:可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高;
乐观锁:十分乐观,认为不会出现问题,无论干什么都不会去上锁,如果出现问题,就再次更新测试值
乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本号然后进行版本号的对比方式实现);
特点:乐观锁是一种并发类型的锁,其本身不对数据进行加锁而是通过业务实现锁的功能,不对数据进行加锁就意味着允许多个请求同时访问数据,同时也省掉了对数据加锁和解锁的过程,这种方式因为节省了悲观锁加锁的操作,所以可以一定程度的的提高操作的性能,不过在并发非常高的情况下,会导致大量的请求冲突,冲突导致大部分操作无功而返而浪费资源,所以在高并发的场景下,乐观锁的性能却反而不如悲观锁。
配置乐观锁
乐观锁实现方式:
-
取出记录时,获取当前version
-
更新时,带上这个version
-
执行更新时,set version = newVersion where version = oldVersion
-
如果version不对,就更新失败
当要更新一条记录时,是希望这条记录没有被更新的
-- 乐观锁:1、先查询,获取版本号 version=1
-- A线程
update user name = "tian" ,version = version +1
where id = 2 and version = 1
-- 如果B线程抢先完成,这个时候version=2,就会导致A线程修改失败
-- B线程
update user name = "tian" ,version = version +1
where id = 2 and version = 1
测试MP的乐观锁插件
- 数据库中添加version字段
- 同步实体类(记得在实体类上加上@Version注解)
@Version //乐观锁version注解
private Integer version;
- 配置插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
首先创建配置类文件config,在该文件下创建配置类MyBatisPlusConfig,该类需要添加三个注解:
@Configuration //配置类
@MapperScan("com.wen.mybatis_plus.mapper") //扫描mapper
@EnableTransactionManagement //自动管理事务,默认是开启的
@MapperScan()是将原先MybatisPlusApplication中的扫描换到这里的,所以MybatisPlusApplication中就不需要@MapperScan(),在该配置类里添加@MapperScan()即可
- 创建完MyBatisPlusConfig类并添加完注解后,就可以将上面的组件的注解方式填入进来
@Configuration //配置类
@MapperScan("com.wen.mybatis_plus.mapper") //扫描mapper
@EnableTransactionManagement
public class MyBatisPlusConfig {
//注册乐观锁插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
此时乐观锁就已经配置完成了!
6、增删改查
查询操作
//测试查询
@Test
public void testSelectById(){
User user = userMapper.selectById(1L);
System.out.println(user);
}
//批量查询
@Test
public void selectBatchIds(){
List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
users.forEach(System.out::println);
}
//条件查询
@Test
public void selectByMap(){
HashMap<String,Object> map = new HashMap<>();
//自定义查询
map.put("name","小文");
map.put("age",20);
List<User> users = userMapper.selectByMap(map);
users.forEach(System.out::println);
}
6.1、分页查询
-
原始的limit进行分页
-
pageHelper第三方插件
-
MyBatisPlus内置分页插件
配置拦截器组件即可
/**
* 注册插件
*/
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor();
// 设置请求的页面大于最大页后操作,true调回到首页,false继续请求。默认false
pageInterceptor.setOverflow(false);
// 单页分页条数限制,默认无限制
pageInterceptor.setMaxLimit(500L);
// 设置数据库类型
pageInterceptor.setDbType(DbType.MYSQL);
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
分页组件测试
//测试MybatisPlus分页插件
@Test
public void testMybatisPlus_Page(){
// 两个参数:current的值默认是1,从1开始,不是0。size是每一页的条数。
Page<User> page = new Page<>(1, 4);
userMapper.selectPage(page,null);
page.getRecords().forEach(System.out::println);
//page的其他方法
System.out.println("当前页:" + page.getCurrent());
System.out.println("总页数:" + page.getPages());
System.out.println("记录数:" + page.getTotal());
System.out.println("是否有上一页:" + page.hasPrevious());
System.out.println("是否有下一页:" + page.hasNext());
}
删除操作
//测试删除
@Test
public void testDeleteById(){
userMapper.deleteById(4L);
}
//批量删除
@Test
public void testDeleteBatchId(){
userMapper.deleteBatchIds(Arrays.asList(1L,2L));
}
//通过map删除
@Test
public void testdeleteByMap(){
Map<String, Object> map = new HashMap<>();
map.put("name","xiaotian");
userMapper.deleteByMap(map);
}
6.2、逻辑删除
物理删除:从数据库中直接删除
逻辑删除:在数据库中没有被删除,而是通过一个变量来让它失效。 deleted=0 ==》deleted=1
管理员可以查看被删除的记录,防止数据丢失,相当于回收站。
- 在数据表中增加一个deleted字段
- 同步实体类,在实体类上加上@TableLogic 注解
@TableLogic //逻辑删除
private Integer deleted;
配置application.yml文件
#配置日志
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2 同步实体类在实体类上加上@TableLogic 注解d的操作)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
6.3、执行SQL分析打印
可输出 SQL 语句以及其执⾏时间,建议开发测试时启⽤该功能,能快速揪出慢查询
注意:PerformanceInterceptor在3.2.0被移除了,如果想进⾏性能分析,⽤第三⽅的,官⽅这样写的“该插件 3.2.0 以上版本移除 推荐使⽤第三⽅扩展 执⾏SQL分析打印 功能”。也就是p6spy。
p6spy依赖引入
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>最新版本</version>
</dependency>
application.yml
spring:
datasource:
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:h2:mem:test
...
注意: driver-class-name 为 p6spy 提供的驱动类 url 前缀为 jdbc:p6spy 跟着冒号为对应数据库连接地址
实际配置为:
spring:
datasource:
username: root
password: 123456
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:mysql:///mybatis_plus?userUnicode=true&characterEncoding=utf-8
spy.properties配置
#3.2.1以上使用
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
#3.2.1以下使用或者不配置
#modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
测试
//测试MybatisPlus分页插件
@Test
public void testMybatisPlus_Page(){
// 两个参数:current的值默认是1,从1开始,不是0。size是每一页的条数。
Page<User> page = new Page<>(2, 4);
userMapper.selectPage(page,null);
page.getRecords().forEach(System.out::println);
//page的其他方法
System.out.println("当前页:" + page.getCurrent());
System.out.println("总页数:" + page.getPages());
System.out.println("记录数:" + page.getTotal());
System.out.println("是否有上一页:" + page.hasPrevious());
System.out.println("是否有下一页:" + page.hasNext());
}
因为在配置文件中设置了慢SQL的检查,为2s,所以这里的查询可以通过
但是只要超过了时长就会抛出异常
7、条件构造器
Wrapper,可以通过其构造复杂的SQL