源码仓库地址:https://gitee.com/DerekAndroid/agan-boot.git
配套免费视频教程 https://study.163.com/course/introduction/1004576013.htm?share=1&shareId=1016481220
github源码地址:https://github.com/agan-java/agan-boot
2.spring-boot是jar包+tomcat
https://gitee.com/DerekAndroid/agan-boot/tree/master/01/agan-boot-helloworld
效果:
三:@SpringBootApplication就只干了一件事
以上所有注解就只干一件事:把bean注册到spring ioc容器。
通过3种方式来实现:
1. @SpringBootConfiguration 通过@Configuration 与@Bean结合,注册到Spring ioc 容器。
2. @ComponentScan 通过范围扫描的方式,扫描特定注解类,将其注册到Spring ioc 容器。
3. @EnableAutoConfiguration 通过spring.factories的配置,来实现bean的注册到Spring ioc 容器。
# 解密@SpringBootApplication启动原理
### 一:本课程目标:
学习spring boot的核心注解@SpringBootApplication,掌握@SpringBootApplication的原理。
### 二:剖析@SpringBootApplication源码
首先我们来分析springboot的启动注解@SpringBootApplication
```
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication
```
@Target(ElementType.TYPE) 注解的目标位置:接口、类、枚举
@Retention(RetentionPolicy.RUNTIME) 注解会在class字节码文件中存在,在运行时可以通过反射获取到
@Documented 用于生成javadoc,默认情况下,javadoc是不包括注解的. 但如果声明注解时指定了 @Documented,
则它会被 javadoc 之类的工具处理,所以注解类型信息也会被包括在生成的文档中。
@Inherited 作用:在类继承关系中,如果子类要继承父类的注解,那么要该注解必须被@Inherited修饰的注解
除了以上常规的几个注解,剩下几个就是springboot的核心注解了。
@SpringBootApplication就是一个复合注解,包括@ComponentScan,和@SpringBootConfiguration,@EnableAutoConfiguration。
### 三:@SpringBootApplication就只干了一件事
以上所有注解就只干一件事:把bean注册到spring ioc容器。
通过3种方式来实现:
1. @SpringBootConfiguration 通过@Configuration 与@Bean结合,注册到Spring ioc 容器。
2. @ComponentScan 通过范围扫描的方式,扫描特定注解类,将其注册到Spring ioc 容器。
3. @EnableAutoConfiguration 通过spring.factories的配置,来实现bean的注册到Spring ioc 容器。
## 一、本课程目标:
在前面的《SpringBoot的入门例子》的课程基础上,我们继续来讲解springboot的常用配置,
即讲解resources包下的application.properties如何使用。
## 二、最常用的配置1:改端口
```
server.port=9090
```
## 三、最常用的配置2:改随机端口
思考问题:固定端口为什么不能用?为什么要改随机端口?
1.如果在用一台服务器上,多个服务如果用同一个端口会造成端口冲突。
2.在现实的微服务(springcloud、dubbo)开发中,开发人员是不用记住ip和端口的.
故,我们一般在真实的开发环境下,是设置一个随机端口,就不用去管理端口了,也不会造成端口冲突。
```
server.port=${random.int[1024,9999]}
```
## 四、自定义属性配置
讲自定义属性配置,就必须讲解@value注解。
@value的作用是:为了简化读取properties文件中的配置值,spring支持@value注解的方式来获取,这种方式大大简化了项目配置,提高业务中的灵活性。
在application.properties的文件下,加入如下配置
```
agan.msg=hi,hello world!!
```
```
@RestController
public class HelloController {
@Value("${agan.msg}")
private String msg;
@GetMapping("msg")
public String getMsg() {
return msg;
}
}
## 一、本课程目标:
学习什么是yml文件?和学习yml语法。
SpringBoot的配置文件有两种,一种是properties结尾的,一种是以yaml或yml文件结尾的。
1. application.properties
1. application.yml
默认情况下是properties结尾的配置文件
配置文件放在src/main/resources目录或者类路径/config/下
## 二、先弄清楚,什么是yml文件?
yml是YAML(YAML Ain't Markup Language)语言的文件,以数据为中心,比json、xml等更适合做配置文件
## 三、对比区别
```
server.port=9090
agan.msg=hi,hello world!!
```
转换为yml配置文件
```
server:
port: 9090
agan:
msg: hi,hello world!!
```
以空格的缩进程度来控制层级关系。(空格个数不重要)
## 一:本课程目标:
学习springboot日志的框架,学完后会设置日志级别、设置日志的存储路径、设置日志的格式等等。
## 二:剖析springboot的日志框架
slf4j
logback、log4j
从springboot的底层框架spring-boot-starter-logging 可以看出,它依赖了3个框架分别为;slf4j、logback、log4j
### 分析1:slf4j、logback、log4j的区别?
1.logback、log4j:是日志实现框架,就是实现怎么记录日志的。
2.slf4j:提供了java中所有的日志框架的简单抽象(日志的门面设计模式),说白了就是一个日志API(没有实现类),它不能单独使用
故:必须结合logback或log4j日志框架来实现。
### 分析2:springboot的日志搭配
springboot2.0默认采用了slf4f+logback的日志搭配。
在开发过程中,我们都是采用了slf4j的api去记录日志,底层的实现就是根据配置logback或log4j日志框架。
##代码添加
private static final Logger logger= LoggerFactory.getLogger(HelloController.class);
##为什么控制台的日志只输出了 info warn error?
因为springboot默认是info级别的
```
logging.level.com.agan.boot=trace
```
## 三:配置日志的生成存储路径和日志名称
在实际的开发中,你不可能一直看着控制台,而且日志会非常大,瞬间就丢失。
故,我们要把日志存储在指定的目录下;
```
#一下配置的效果为:项目根目录下/output/logs/spring.log,默认的日志名为spring.log
#logging.path=output/logs
# 如果不想要把日志存放在longging.path默认的根目录下,那就采用自定义的目录和文件名
logging.file=/Volumes/data/logs/springboot.log
```
## 三:配置日志的内容格式
```
# %d-时间格式、%thread-线程、%-5level-从左5字符宽度、%logger{50}-日志50个字符、%msg-信息、%n-换行
# 设置在控制台输出的日志格式
logging.pattern.console=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} -%msg%n
# 设置输出到文件的日志格式
logging.pattern.file=%d{yyyy/MM/dd} === [%thread] == %-5level == %logger{50} == %msg%n
代码:
@RestController
public class HelloController {
private static final Logger logger= LoggerFactory.getLogger(HelloController.class);
@Value("${agan.msg}")
private String msg;
@GetMapping("msg")
public String getMsg() {
return msg;
}
@GetMapping("/home")
public String home(){
return "hello agan!";
}
@GetMapping("/log")
public void log(){
logger.trace("------------trace-----------");
logger.debug("------------debug-----------");
logger.info("------------info-----------");
logger.warn("------------warn-----------");
logger.error("------------error-----------");
}
}
1.集成到springboot的yml格式配置文件的示例:
logging:
config: classpath:logback-spring.xml
level:
# 根目录输出warn级别日志
root: warn
# 当前包名输出trace级别日志
com.example.demo: trace
# pattern:
# console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
2. logback-spring.xml配置文件
testFile.log
true
%-4relative [%thread] %-5level %logger{35} - %msg%n
%d{yyyy-MM-dd HH:mm:ss} [%X{x-log-requestid}] [%thread] %-5level %logger{50} - %msg%n
utf8
效果:
参考:https://www.jianshu.com/p/bea462da206b
https://www.cnblogs.com/magic-s/p/9155288.html
https://www.cnblogs.com/gavincoder/p/10091757.html
https://www.cnblogs.com/javalanger/p/10964603.html
#在springboot中使用lombok
##一、本课程目标:
1.学会安装lombok插件,并学会用lombok。
2.掌握lombok的核心@Data注解
3.掌握lombok的核心@Slf4j注解
##二、为什么要使用lombok,它解决了什么问题?
Lombok 是一个 IDEA 插件,也是一个依赖jar 包。
它解决了开发人员少写代码,提升开发效率。
它使开发人员不要去写javabean的getter/setter方法,写构造器、equals等方法;最方便的是你对javabean的属性增删改,
你不用再重新生成getter/setter方法。省去一大麻烦事。
##三、idea安装lombok插件
###步骤1:idea搜索lombok插件
打开IDEA的Settings面板,并选择Plugins选项,然后点击 “Browse repositories..”
![image](https://github.com/agan-java/images/blob/master/lombok/14.png?raw=true)
###步骤2:安装并重启idea
点击安装,然后安装提示重启IDEA,安装成功;
![image](https://github.com/agan-java/images/blob/master/lombok/15.png?raw=true)
记得重启IDEA,不然不生效。
### 四、体验lombok核心注解@data
#### 步骤1: 什么是@data注解
@Data 注解在实体类上,自动生成javabean的getter/setter方法,写构造器、equals等方法;
#### 步骤2:pom文件添加依赖包
```
org.projectlombok
lombok
1.18.8
```
### 五、体验lombok第二核心注解@Slf4j
注解@Slf4j的作用就是代替一下代码
```
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
```
让你不用每次都写重复的代码
代码
@RestController
@Slf4j
public class UserController {
//private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@RequestMapping("/user")
public UserVO user() {
UserVO userVO=new UserVO();
userVO.setId(100);
userVO.setUsername("agan");
return userVO;
}
@RequestMapping("/log")
public void log() {
log.trace("-----------trace-------------");
log.debug("-----------debug-------------");
log.info("-----------info-------------");
log.warn("-----------warn-------------");
log.error("-----------error-------------");
}
}
使用代替包兼容slf4j
Spring Boot 异步框架
### 一、课程目标
熟悉spring的异步框架,学会使用异步@Async注解
### 二、为什么要用异步框架,它解决什么问题?
在SpringBoot的日常开发中,一般都是同步调用的。但经常有特殊业务需要做异步来处理,例如:注册新用户,送100个积分,或下单成功,发送push消息等等。
就拿注册新用户为什么要异步处理?
- 第一个原因:容错性、健壮性,如果送积分出现异常,不能因为送积分而导致用户注册失败;
因为用户注册是主要功能,送积分是次要功能,即使送积分异常也要提示用户注册成功,然后后面在针对积分异常做补偿处理。
- 第二个原因:提升性能,例如注册用户花了20毫秒,送积分花费50毫秒,如果用同步的话,总耗时70毫秒,用异步的话,无需等待积分,故耗时20毫秒。
故,异步能解决2个问题,性能和容错性。
### 三、SpringBoot异步调用
在SpringBoot中使用异步调用是很简单的,只需要使用@Async注解即可实现方法的异步调用。
### 四、@Async异步调用例子
#### 步骤1:开启异步任务
采用@EnableAsync来开启异步任务支持,另外需要加入@Configuration来把当前类加入springIOC容器中。
```
@Configuration
@EnableAsync
public class SyncConfiguration {
}
```
#### 步骤2:在方法上标记异步调用
增加一个service类,用来做积分处理。
@Async添加在方法上,代表该方法为异步处理。
```
public class ScoreService {
private static final Logger logger = LoggerFactory.getLogger(ScoreService.class);
@Async
public void addScore(){
//TODO 模拟睡5秒,用于赠送积分处理
try {
Thread.sleep(1000*5);
logger.info("--------------处理积分--------------------");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
```
### 五、为什么要给@Async自定义线程池?
@Async注解,在默认情况下用的是SimpleAsyncTaskExecutor线程池,该线程池不是真正意义上的线程池,因为线程不重用,每次调用都会新建一条线程。
可以通过控制台日志输出查看,每次打印的线程名都是[task-1]、[task-2]、[task-3]、[task-4].....递增的。
@Async注解异步框架提供多种线程
SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方
ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类
ThreadPoolTaskScheduler:可以使用cron表达式
ThreadPoolTaskExecutor :最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装
### 六、为@Async实现一个自定义线程池
#### 步骤1:配置线程池
```
@Configuration
@EnableAsync
public class SyncConfiguration {
@Bean(name = "scorePoolTaskExecutor")
public ThreadPoolTaskExecutor getScorePoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//核心线程数
taskExecutor.setCorePoolSize(10);
//线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
taskExecutor.setMaxPoolSize(100);
//缓存队列
taskExecutor.setQueueCapacity(50);
//许的空闲时间,当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
taskExecutor.setKeepAliveSeconds(200);
//异步方法内部线程名称
taskExecutor.setThreadNamePrefix("score-");
/**
* 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
* 通常有以下四种策略:
* ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
* ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
* ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
* ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}
```
#### 步骤2: 为@Async指定线程池名字
```
@Async("scorePoolTaskExecutor")
public void addScore2(){
//TODO 模拟睡5秒,用于赠送积分处理
try {
Thread.sleep(1000*5);
log.info("--------------处理积分--------------------");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
```
效果:
# SpringBoot集成swagger实战
### 一、本课程目标:
1. 弄清楚,为什么要用swagger,它解决了什么问题?
2. 编码实现2个springboot接口,让swagger自动生成接口文档
### 二、为什么要用swagger,它解决了什么问题?
随着sprnigboot、springcloud等微服务的流行,在微服务的设计下,小公司微服务小的几十,大公司大的几百上万的微服务。
这么多的微服务必定产生了大量的接口调用。而接口的调用就必定要写接口文档。
在微服务的盛行下,成千上万的接口文档编写,不可能靠人力来编写,故swagger就产生了,它采用自动化实现并解决了人力编写接口文档的问题;
它通过在接口及实体上添加几个注解的方式就能在项目启动后自动化生成接口文档,
Swagger 提供了一个全新的维护 API 文档的方式,有4大优点:
1.自动生成文档:只需要少量的注解,Swagger 就可以根据代码自动生成 API 文档,很好的保证了文档的时效性。
2.跨语言性,支持 40 多种语言。
3.Swagger UI 呈现出来的是一份可交互式的 API 文档,我们可以直接在文档页面尝试 API 的调用,省去了准备复杂的调用参数的过程。
4.还可以将文档规范导入相关的工具(例如 SoapUI), 这些工具将会为我们自动地创建自动化测试。
### 三、案例实战:把springboot的接口,自动生成接口文档
#### 步骤1: pom文件加入依赖包
```
io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
```
#### 步骤2:修改配置文件
1. application.properties 加入配置
```
#表示是否开启 Swagger,一般线上环境是关闭的
spring.swagger2.enabled=true
```
2.增加一个swagger配置类
```
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Value(value = "${spring.swagger2.enabled}")
private Boolean swaggerEnabled;
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(swaggerEnabled)
.select()
.apis(RequestHandlerSelectors.basePackage("com.agan.boot"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("接口文档")
.description("阿甘讲解 Spring Boot")
.termsOfServiceUrl("https://study.163.com/provider/1016671292/index.htm")
.version("1.0")
.build();
}
}
```
以上注意点:
1.createRestApi() 这个方法一定要写上你的包名名,代表需要生成接口文档的目录包
体验地址:http://127.0.0.1:9090/swagger-ui.html
swagger常用注解
Swagger 常用注解
注解 用途 注解位置
@Api 描述类的作用 注解于类上
@ApiOperation 描述类的方法的作用 注解于方法上
@ApiParam 描述类方法参数的作用 注解于方法的参数上
@ApiModel 描述对象的作用 注解于请求对象或者返回结果对象上
@ApiModelProperty 描述对象里字段的作用 注解于请求对象或者返回结果对象里的字段上
对象注解示例:
@ApiModel(value = "用户信息")
@Data
public class UserVO {
@ApiModelProperty(value = "用户ID")
private Integer id;
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "性别 0=女 1=男 ")
private Byte sex;
@ApiModelProperty(value = "删除标志,默认0不删除,1删除")
private Byte deleted;
@ApiModelProperty(value = "更新时间")
private Date updateTime;
@ApiModelProperty(value = "创建时间")
private Date createTime;
}
本地体验地址:http://127.0.0.1:9090/swagger-ui.html
# MyBatis 代码生成器
## 一、什么是MyBatis 代码生成器?
MyBatis Generator(简称为:MyBatis 代码生成器) 是MyBatis 官方出品的一款,用来自动生成MyBatis的 mapper、xml、entity 的框架,
让程序员在开发的过程中省去很多重复的工作。
操作非常简单,只要在配置文件中,配置好要生成的表名和包名,然后运行命令,就能自动生成mapper、xml、entity 等一堆文件。
官网地址:http://www.mybatis.org/generator/
## 二、MyBatis Generator搭建步骤
### 步骤1:新建数据库
```
CREATE DATABASE `boot_user` /*!40100 DEFAULT CHARACTER SET utf8 */;
use boot_user;
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL DEFAULT '' COMMENT '用户名',
`password` varchar(50) NOT NULL DEFAULT '' COMMENT '密码',
`sex` tinyint(4) NOT NULL DEFAULT '0' COMMENT '性别 0=女 1=男 ',
`deleted` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '删除标志,默认0不删除,1删除',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户表';
SET FOREIGN_KEY_CHECKS = 1;
```
### 步骤2:pom.xml依赖包
1. pom.xml的插件
加入 mybatis-generator-maven-plugin
```
${basedir}/src/main/java
**/*.xml
${basedir}/src/main/resources
maven-compiler-plugin
${jdk.version}
org.mybatis.generator
mybatis-generator-maven-plugin
1.3.6
${basedir}/src/main/resources/generatorConfig.xml
true
true
mysql
mysql-connector-java
${mysql.version}
tk.mybatis
mapper
${mapper.version}
```
2. pom.xml加入依赖包
```
org.mybatis.generator
mybatis-generator-core
1.3.2
compile
true
tk.mybatis
mapper
${mapper.version}
```
### 步骤3:2个核心的配置文件
resources配置文件 config.properties
重点要注意:生成的包名
```
# 生成的包名
package.name=com.agan.boot
# 数据库配置信息
jdbc.driverClass = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://192.168.0.138:3308/boot_user
jdbc.user = root
jdbc.password =agan
```
最核心的配置文件generatorConfig.xml
```
```
以上配置 ,一定要修改以下内容
```
```
### 步骤4:运行generator插件
双击 mybatis-generator:generate# MyBatis 代码生成器
1 sql执行报错 Incorrect table definition; there can be only oneTIMESTAMP column with CURRENT_TIMESTAMP in DEFAULT or ON UPDATEclause
解决:修改时间戳,表中存在两个DEFAULT CURRENT_TIMESTAMP属性
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`create_time` TIMESTAMP NOT NULL COMMENT '创建时间',
解决方法2:MySql 5.5和MySql 5.6之后版本的区别:5.5 只能有一个Timestamp,将其中一列类型改为datetime类型就可以解决
2 Generator插件找不到,如何运行
解决:为当前项目手动添加配置maven任务
command命令:
mybatis-generator:generate -e
3 自动生成的效果:
agan-boot-mybatis-multi-datasource
# 什么是druid,它解决了什么问题?
Druid是阿里巴巴开源平台上的一个项目,整个项目由数据库连接池、插件框架和SQL解析器组成。
druid主要是为了解决JDBC的一些限制,可以让程序员实现一些特殊的需求,比如向密钥服务请求凭证、统计SQL信息、
SQL性能收集、SQL注入检查、SQL翻译等,程序员可以通过定制来实现自己需要的功能。
官方地址:https://github.com/alibaba/druid
为监控而生的数据库连接池,重点是有个后台监控系统:http://localhost:9090/druid
# 采用druid实现多数据源
## 本案例的多数据源业务场景介绍
在编码之前,我们先来讲解我们这个例子的业务场景。
就用一个微信发红包大家都熟悉来讲解,你用余额给你的好友发了10元红包,
假如:余额和红包分别是2个独立数据库。
这个时候你的余额数据库对应的余额就应该减10元,你的好友的红包数据库就应该加10元。
## 步骤1:建立2个数据库
```
CREATE DATABASE `xa_account` /*!40100 DEFAULT CHARACTER SET utf8 */;
use xa_account;
CREATE TABLE `capital_account` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`user_id` int(10) NOT NULL COMMENT '用户ID',
`balance_amount` decimal(10,0) DEFAULT '0' COMMENT '账户余额',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='账户信息表';
INSERT INTO `capital_account` (`id`,`user_id`,`balance_amount`) VALUES (1,1,2000);
CREATE DATABASE `xa_red_account` /*!40100 DEFAULT CHARACTER SET utf8 */;
use xa_red_account;
CREATE TABLE `red_packet_account` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`user_id` int(10) NOT NULL COMMENT '用户ID',
`balance_amount` decimal(10,0) DEFAULT '0' COMMENT '账户余额',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='红包账户信息表';
INSERT INTO `red_packet_account` (`id`,`user_id`,`balance_amount`) VALUES (1,2,1000);
```
## 步骤2:pom文件加入依赖包
```
tk.mybatis
mapper-spring-boot-starter
2.0.3
com.alibaba
druid-spring-boot-starter
1.1.18
mysql
mysql-connector-java
```
## 步骤3:修改配置文件
application.properties 必须配置2个数据源
```
# 数据源配置
# 数据源 account
spring.datasource.druid.account.url=jdbc:mysql://192.168.0.138:3308/xa_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
spring.datasource.druid.account.username=root
spring.datasource.druid.account.password=agan
spring.datasource.druid.account.driver-class-name=com.mysql.jdbc.Driver
#初始化连接大小:连接池建立时创建的初始化连接数
spring.datasource.druid.account.initial-size=5
#最小空闲连接数:连接池中最小的活跃连接数
spring.datasource.druid.account.min-idle=15
#最大连接数:连接池中最大的活跃连接数
spring.datasource.druid.account.max-active=60
spring.datasource.druid.account.validation-query=SELECT 1
#获取连接时检测:是否在获得连接后检测其可用性
spring.datasource.druid.account.test-on-borrow=true
#空闲时检测:是否在连接空闲一段时间后检测其可用性
spring.datasource.druid.account.test-while-idle=true
#连接保持空闲而不被驱逐的最长时间
spring.datasource.druid.account.time-between-eviction-runs-millis=60000
#数据源 redpacket
spring.datasource.druid.redpacket.url=jdbc:mysql://192.168.0.138:3308/xa_red_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
spring.datasource.druid.redpacket.username=root
spring.datasource.druid.redpacket.password=agan
spring.datasource.druid.redpacket.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.redpacket.initial-size=5
spring.datasource.druid.redpacket.min-idle=15
spring.datasource.druid.redpacket.max-active=60
spring.datasource.druid.redpacket.validation-query=SELECT 1
spring.datasource.druid.redpacket.test-on-borrow=true
spring.datasource.druid.redpacket.test-while-idle=true
spring.datasource.druid.redpacket.time-between-eviction-runs-millis=60000
# 合并多个datasource监控
spring.datasource.druid.use-global-data-source-stat=true
#配置druid显示监控统计信息
#开启Druid的监控平台 http://localhost:9090/druid
#1. StatViewServlet配置,说明请参考Druid Wiki,配置_StatViewServlet配置
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
spring.datasource.druid.stat-view-servlet.reset-enable=false
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=agan
#spring.datasource.druid.stat-view-servlet.allow=
#spring.datasource.druid.stat-view-servlet.deny=
#2. WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter
spring.datasource.druid.web-stat-filter.enabled=true
spring.datasource.druid.web-stat-filter.url-pattern=/*
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
#spring.datasource.druid.web-stat-filter.session-stat-enable=
#spring.datasource.druid.web-stat-filter.session-stat-max-count=
#spring.datasource.druid.web-stat-filter.principal-session-name=
#spring.datasource.druid.web-stat-filter.principal-cookie-name=
#spring.datasource.druid.web-stat-filter.profile-enable=
#3. Spring监控配置,说明请参考Druid Github Wiki,配置_Druid和Spring关联监控配置
#spring.datasource.druid.aop-patterns= # Spring监控AOP切入点,如x.y.z.service.*,配置多个英文逗号分隔
#Spring Boot2.1以上 默认禁用那种bean覆盖(作用 用于兼容低版本)
spring.main.allow-bean-definition-overriding=true
```
## 步骤4:将配置信息,注入druid
1. 配置2个数据源DataSource
```
@Configuration
@EnableConfigurationProperties
@EnableTransactionManagement(proxyTargetClass = true)
public class MybatisConfiguration {
/**
* account数据库配置前缀.
*/
final static String ACCOUNT_PREFIX = "spring.datasource.druid.account";
/**
* redpacket数据库配置前缀.
*/
final static String REDPACKET_PREFIX = "spring.datasource.druid.redpacket";
/**
* 配置Account数据库的数据源
*
* @return the data source
*/
@Bean(name = "AccountDataSource")
@ConfigurationProperties(prefix = ACCOUNT_PREFIX) // application.properties中对应属性的前缀
public DataSource accountDataSource() {
return DruidDataSourceBuilder.create().build();
}
/**
* 配置RedPacket数据库的数据源
*
* @return the data source
*/
@Bean(name = "RedPacketDataSource")
@ConfigurationProperties(prefix = REDPACKET_PREFIX) // application.properties中对应属性的前缀
public DataSource redPacketDataSource() {
return DruidDataSourceBuilder.create().build();
}
}
```
2.有了数据源,就要配置数据源的sessionFactory
配置account数据源的sessionFactory
```
@Configuration
@MapperScan(basePackages = {"com.agan.boot.mapper.account.mapper"}, sqlSessionFactoryRef = "accountSqlSessionFactory")
public class AccountDataSourceConfiguration {
/**
* mybatis的xml文件.
*/
public static final String MAPPER_XML_LOCATION = "classpath*:com/agan/boot/mapper/account/mapper/*.xml";
@Autowired
@Qualifier("AccountDataSource")
DataSource accountDataSource;
/**
* 配置Sql Session模板
*/
@Bean
public SqlSessionTemplate springSqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(accountSqlSessionFactory());
}
/**
* 配置SQL Session工厂
*/
@Bean
public SqlSessionFactory accountSqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(accountDataSource);
//指定XML文件路径
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_XML_LOCATION));
return factoryBean.getObject();
}
/**
* 配置事务
*/
@Bean(name="transactionManager")
public DataSourceTransactionManager transactionManager(){
return new DataSourceTransactionManager(accountDataSource);
}
}
```
配置redaccount数据源的sessionFactory
```
@Configuration
@MapperScan(basePackages = {"com.agan.boot.mapper.redaccount.mapper"}, sqlSessionFactoryRef = "redPacketSqlSessionFactory")
public class RedAccountDataSourceConfiguration {
/**
* mybatis的xml文件.
*/
public static final String MAPPER_XML_LOCATION = "classpath*:com/agan/boot/mapper/redaccount/mapper/*.xml";
@Autowired
@Qualifier("RedPacketDataSource")
DataSource redPacketDataSource;
/**
* 配置Sql Session模板
*/
@Bean
public SqlSessionTemplate redPacketSqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(redPacketSqlSessionFactory());
}
/**
* 配置SQL Session工厂
*/
@Bean
public SqlSessionFactory redPacketSqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(redPacketDataSource);
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_XML_LOCATION));
return factoryBean.getObject();
}
/**
* 配置事务
*/
@Bean(name="transactionManager")
public DataSourceTransactionManager transactionManager(){
return new DataSourceTransactionManager(redPacketDataSource);
}
}
```
## 步骤5:生成account表和 redpacket表对应的mybatis文件
采用mybatis-generator来自动生成代码,具体见《MyBatis 代码生成器Generator》
## 步骤6:service 体验类
PayService 作用:模拟发红包,账户余额减钱,红包余额加钱
```
@Service
public class PayService {
@Autowired
private CapitalAccountMapper capitalAccountMapper;
@Autowired
private RedPacketAccountMapper redPacketAccountMapper;
/**
* 账户余额 减钱
* @param userId
* @param account
*/
@Transactional(rollbackFor = Exception.class)
public void payAccount(int userId,int account) {
CapitalAccount ca=new CapitalAccount();
ca.setUserId(userId);
CapitalAccount capitalDTO=this.capitalAccountMapper.selectOne(ca);
// System.out.println(capitalDTO);
//从账户里面扣除钱
capitalDTO.setBalanceAmount(capitalDTO.getBalanceAmount()-account);
this.capitalAccountMapper.updateByPrimaryKey(capitalDTO);
}
/**
* 红包余额 加钱
* @param userId
* @param account
*/
@Transactional(rollbackFor = Exception.class)
public void payRedPacket(int userId,int account) {
RedPacketAccount red= new RedPacketAccount();
red.setUserId(userId);
RedPacketAccount redDTO=this.redPacketAccountMapper.selectOne(red);
// System.out.println(redDTO);
//红包余额 加钱
redDTO.setBalanceAmount(redDTO.getBalanceAmount()+account);
this.redPacketAccountMapper.updateByPrimaryKey(redDTO);
//int i=9/0;
}
@Transactional(rollbackFor = Exception.class)
public void pay(int fromUserId,int toUserId,int account){
//账户余额 减钱
this.payAccount(fromUserId,account);
//红包余额 加钱
this.payRedPacket(toUserId,account);
}
}
```
## 步骤7:controller测试体验类
```
@RestController
public class PayController {
@Autowired
private PayService payService;
@RequestMapping(value = "/pay", method = RequestMethod.GET)
public void create(){
this.payService.pay(1,2,10);
}
}
```
@Configuration 作为 Spring对Bean的显示配置,用于构建bean的定义。
官方解析:@Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。
@ConfigurationProperties 是对"@Configuration"注解类的属性配置说明。如:@ConfigurationProperties(prefix="spring.datasource.druid.account")。这样该注解类的属性就可以配置在文件中,如以上类“KNApiProviderConfig”的属性可以在application.xml配置。
理解:
1 就是就是把application.properties配置信息和Bean为AccountDataSource类绑定起来
spring.datasource.druid.account.username=root
spring.datasource.druid.account.password=123
spring.datasource.druid.account.driver-class-name=com.mysql.jdbc.Driver
@Configuration
@EnableConfigurationProperties
@EnableTransactionManagement(proxyTargetClass = true)
public class MybatisConfiguration {
/**
* account数据库配置前缀.
*/
final static String ACCOUNT_PREFIX = "spring.datasource.druid.account";
/**
* redpacket数据库配置前缀.
*/
final static String REDPACKET_PREFIX = "spring.datasource.druid.redpacket";
/**
* 配置Account数据库的数据源
*
* @return the data source
*/
@Bean(name = "AccountDataSource")
@ConfigurationProperties(prefix = ACCOUNT_PREFIX) // application.properties中对应属性的前缀
public DataSource accountDataSource() {
return DruidDataSourceBuilder.create().build();
}
/**
* 配置RedPacket数据库的数据源
*
* @return the data source
*/
@Bean(name = "RedPacketDataSource")
@ConfigurationProperties(prefix = REDPACKET_PREFIX) // application.properties中对应属性的前缀
public DataSource redPacketDataSource() {
return DruidDataSourceBuilder.create().build();
}
}
2 @Qualifier 对Bean的命名.就是使用这个类
@Configuration
@MapperScan(basePackages = {"com.agan.boot.mapper.account.mapper"}, sqlSessionFactoryRef = "accountSqlSessionFactory")
public class AccountDataSourceConfiguration {
/**
* mybatis的xml文件.
*/
public static final String MAPPER_XML_LOCATION = "classpath*:com/agan/boot/mapper/account/mapper/*.xml";
@Autowired
@Qualifier("AccountDataSource")
DataSource accountDataSource;
# SpringBoot集成mybatis攻略
## 一、课程目标:
1. 为什么使用 MyBatis ?
2. 为什么需要通用的tk mapper ?
3. 感受《MyBatis 代码生成器Generator》自动生成代码的快感
4. 体验不用写SQL的mybatis通用增删改查操作
## 一、什么是 MyBatis?
MyBatis 是一款优秀的持久层框架,支持常规的SQL查询,同时也支持定制化SQL、存储过程以及高级映射。
MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及对结果集的检索封装。
MyBatis可以使用简单的XML或注解用于配置和原始映射,将接口和Java的POJO(Plain Old Java Objects,普通的Java对象)映射成数据库中的记录。
## 二、为什么使用 MyBatis
在我们传统的 JDBC 中,我们除了需要自己提供 SQL 外,还必须操作 Connection、Statment、ResultSet,不仅如此,为了访问不同的表,不同字段的数据,
我们需要些很多雷同模板化的代码,即繁琐又枯燥。
而我们在使用了 MyBatis 之后,只需要提供 SQL 语句就好了,其余的诸如:建立连接、操作 Statment、ResultSet,处理 JDBC 相关异常等等都可以交给
MyBatis 去处理,我们的关注点于是可以就此集中在 SQL 语句上,关注在增删改查这些操作层面上。
并且 MyBatis 支持使用简单的 XML 或注解来配置和映射原生信息,
将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。
一句话:不用写jdbc代码,只写sql就行!!!
## 四、什么是通用的tk mapper ?
在使用MyBatis 的同时,建议大家再搭配使用"通用tk Mapper4",它是一个可以实现任意 MyBatis 通用方法的框架,
项目提供了常规的增删改查操作以及Example 相关的单表操作。通用 Mapper 是为了解决 MyBatis 使用中 90% 的基本操作,
使用它可以很方便的进行开发,可以节省开发人员大量的时间。
官方地址:https://github.com/abel533/Mapper/wiki
## 三、那为什么需要通用的tk mapper ?
mybatis最大的一个问题,就是要写大量的SQL在XML中,因为除了必须的特殊复杂的业务逻辑SQL外,还要为大量的类似增删改查SQL。
另外,当数据库表结构变更时,所有对应的SQL和实体类都要改一遍,那个痛苦啊。故,通用tk mapper应运而生。
一句话:不用写jdbc代码,同时也不用写sql !(对于互联网公司来说,97%不是写sql,剩下3%要写多表关联查询sql)
## 四、案例实战:SpringBoot配置mybatis的步骤
### 步骤1:pom文件引入依赖包
```
tk.mybatis
mapper-spring-boot-starter
2.0.3
mysql
mysql-connector-java
org.projectlombok
lombok
1.18.8
```
### 步骤2:采用mybatis-generator自动生成代码
拷贝《案例实战:MyBatis 代码生成器Generator》生成的代码即可
### 步骤3:application.properties 加入mysql配置信息
```
#指定mapper.xml的位置
mybatis.mapper-locations=classpath*:com/agan/boot/mapper/xml/*.xml
#数据库驱动和ip
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.0.138:3308/boot_user?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
spring.datasource.username=root
spring.datasource.password=agan
```
特别注意:一定要配置mybatis.mapper-locations mybatis的xml路径,不然启动不了!!!
### 步骤4:配置扫描mapper类的路径包
在启动类中,加入mybatis的mapper
@MapperScan("com.agan.boot.mapper")
如果不配的话,mybatis找不到UserMapper文件
```
//指定要扫描的Mapper类的包的路径
@MapperScan("com.agan.boot.mapper")
@SpringBootApplication
public class MybatisApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisApplication.class, args);
}
}
```
### 步骤5:测试类
UserController
Controller
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 初始化1000条数据
*/
@RequestMapping(value = "/c", method = RequestMethod.GET)
public void create(){
for(int i=0;i<1000;i++){
User user=new User();
String temp="user"+i;
user.setUsername(temp);
user.setPassword(temp);
Random random=new Random();
int sex=random.nextInt(2);
user.setSex((byte)sex);
userService.createUser(user);
}
}
/**
* 修改某条数据
* @param id
*/
@RequestMapping(value = "/u/{id}", method = RequestMethod.GET)
public void create(@PathVariable int id){
User user=new User();
user.setId(id);
String temp="update"+id;
user.setUsername(temp);
user.setPassword(temp);
this.userService.updateUser(user);
}
/**
* 查询例子
*/
@RequestMapping(value = "/f", method = RequestMethod.GET)
public void find(){
this.userService.findExample();
}
}
Service
@Service
@Slf4j
public class UserService {
@Autowired
private UserMapper userMapper;
public void createUser(User obj){
userMapper.insertSelective(obj);
}
public void updateUser(User obj){
this.userMapper.updateByPrimaryKeySelective(obj);
}
public void findExample(){
log.info("----------------按主键查询: where id=100----------------");
User user=this.userMapper.selectByPrimaryKey(100);
log.info(user.toString());
log.info("----------------查询: where sex=1----------------");
User sex=new User();
sex.setSex((byte)1);
List list=this.userMapper.select(sex);
log.info("查询sex=1的条数,{}",list.size());
log.info("----------------查询: where username=? and password=?----------------");
User user1=new User();
user1.setUsername("update100");
user1.setPassword("update100");
User obj=this.userMapper.selectOne(user1);
log.info(obj.toString());
/**
* 复杂查询用Example.Criteria
*/
log.info("----------------Example.Criteria查询: where username=? and password=?----------------");
Example example=new Example(User.class);
Example.Criteria criteria=example.createCriteria();
criteria.andEqualTo("username","update100");
criteria.andEqualTo("password","update100");
List objs=this.userMapper.selectByExample(example);
log.info("Example.Criteria查询结果,{}",objs.toString());
log.info("----------------Example.Criteria 模糊查询: where username like ? ----------------");
example=new Example(User.class);
criteria=example.createCriteria();
criteria.andLike("username","%100%");
objs=this.userMapper.selectByExample(example);
log.info("Example.Criteria查询结果,{}",objs.toString());
log.info("----------------Example.Criteria 排序: where username like ? order by id desc ----------------");
example=new Example(User.class);
example.setOrderByClause("id desc ");
criteria=example.createCriteria();
criteria.andLike("username","%100%");
objs=this.userMapper.selectByExample(example);
log.info("Example.Criteria查询结果,{}",objs.toString());
log.info("----------------Example.Criteria in 查询: where id in (1,2) ----------------");
example=new Example(User.class);
criteria=example.createCriteria();
List ids=new ArrayList();
ids.add(1);
ids.add(2);
criteria.andIn("id",ids);
objs=this.userMapper.selectByExample(example);
log.info("Example.Criteria查询结果,{}",objs.toString());
log.info("----------------分页查询1----------------");
User obj2=new User();
obj2.setSex((byte)1);
int count=this.userMapper.selectCount(obj2);
log.info("分页例子:总条数{}",count);
objs=this.userMapper.selectByRowBounds(obj2,new RowBounds(0,10));
for (User u:objs){
log.info("分页例子:第一页{}",u.toString());
}
log.info("----------------Example.Criteria分页查询2----------------");
example=new Example(User.class);
criteria=example.createCriteria();
criteria.andEqualTo("sex",1);
count=this.userMapper.selectCountByExample(example);
log.info("分页例子:总条数{}",count);
objs=this.userMapper.selectByExampleAndRowBounds(example,new RowBounds(0,10));
for (User u:objs){
log.info("分页例子:第一页{}",u.toString());
}
}
}
推荐使用aop切面拦截+注解引入使用
### 本课程目标:
1.弄懂为什么要限流,学会限流的技巧。
2.了解限流的令牌桶算法
3.编码实现guava的SpringBoot限流
4.为了提升开发效率,降低代码耦合度,采用自定义注解来实现接口限流
### 互联网系统为什么要限流?
因为互联网系统通常都要面对大并发大流量的请求,在突发情况下,系统是抗不住的,例如,抢票系统 12306,在面对高并发的情况下,就是采用了限流。
例如:12306限流后,就进入了服务降级,在流量高峰期间经常会出现,这的提示语;"当前排队人数较多,请稍后再试!"
故,针对高并发流量来说,限流是最好的保护机制。
### 那什么是限流?如何实现?
限流是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法实现流量限制,使用十分方便,而且十分高效。
### 重点讲解令牌桶算法
令牌算法:
假设有一个木桶,这个木桶干了2件事:
1.系统按恒定1/QPS的时间顺序,往桶里放入令牌token;如果桶加满了就不加。
2.当新请求过来时,会在桶里拿走一个token,如果没有token可以拿,就阻塞或拒绝服务。
![image](https://github.com/agan-java/images/blob/master/limiter/01.png?raw=true)
![image](https://github.com/agan-java/images/blob/master/limiter/02.jpg?raw=true)
### 采用guava实现SpringBoot限流
#### 步骤1:pom文件加入guava依赖包
```
com.google.guava
guava
28.1-jre
```
#### 步骤2:加入限流逻辑
```
@RestController
@Slf4j
public class TestController {
//限流,1秒钟2个
private RateLimiter limiter = RateLimiter.create(2);
private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@GetMapping("/limiter")
public String testLimiter() {
//500毫秒内,没拿到令牌,就直接进入服务降级
boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);
if (!tryAcquire) {
log.warn("进入服务降级,时间{}", simpleDateFormat.format(new Date()));
return "当前排队人数较多,请稍后再试!";
}
log.info("获取令牌成功,时间{}", simpleDateFormat.format(new Date()));
return "success";
}
}
```
以上用到了RateLimiter的2个核心方法:create()、tryAcquire(),以下详细说明
- acquire() 获取一个令牌, 改方法会阻塞直到获取到这一个令牌, 返回值为获取到这个令牌花费的时间
- acquire(int permits) 获取指定数量的令牌, 该方法也会阻塞, 返回值为获取到这 N 个令牌花费的时间
- tryAcquire() 判断时候能获取到令牌, 如果不能获取立即返回 false
- tryAcquire(int permits) 获取指定数量的令牌, 如果不能获取立即返回 false
- tryAcquire(long timeout, TimeUnit unit) 判断能否在指定时间内获取到令牌, 如果不能获取立即返回 false
- tryAcquire(int permits, long timeout, TimeUnit unit) 同上
#### 步骤3:体验效果
浏览器反复刷新,此测试地址: http://127.0.0.1:9090/limiter
```
进入服务降级,时间2019-09-24 18:17:59
获取令牌成功,时间2019-09-24 18:17:59
进入服务降级,时间2019-09-24 18:17:59
进入服务降级,时间2019-09-24 18:17:59
获取令牌成功,时间2019-09-24 18:17:59
进入服务降级,时间2019-09-24 18:17:59
进入服务降级,时间2019-09-24 18:18:00
获取令牌成功,时间2019-09-24 18:18:00
进入服务降级,时间2019-09-24 18:18:00
进入服务降级,时间2019-09-24 18:18:00
获取令牌成功,时间2019-09-24 18:18:00
```
从以上日志可以看出,1秒钟内只有2次成功,其他都失败降级了。
### 启用自定义注解实现接口限流
为什么要自定义注解来实现接口限流?
1. 业务代码和限流代码解耦,开发人员只要一个注解,不用关心限流的实现逻辑。
1. 采用实现自定义注解,减少代码冗余。
#### 步骤1:pom文件加入aop依赖包
```
org.springframework.boot
spring-boot-starter-aop
```
#### 步骤2:实现一个限流自定义注解
```
```
#### 步骤3:用切面拦截限流注解
```
```
#### 步骤3:实现限流接口
```
@GetMapping("/limiter2")
@Limiter(key = "limiter2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "当前排队人数较多,请稍后再试!")
public String limiter2() {
log.debug("令牌桶=limiter2,获取令牌成功");
return "ok";
}
@GetMapping("/limiter3")
@Limiter(key = "limiter3", permitsPerSecond = 2, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "当前排队人数较多,请稍后再试!")
public String limiter3() {
log.debug("令牌桶=limiter3,获取令牌成功");
return "ok";
}
```
##:课后练习
guava 的RateLimiter提供了2个方法tryAcquire()和acquire()来实现限流;
我们本课程采用的是tryAcquire()来实现限流,如果采用 acquire(),会怎么样? 请编码实现。
1 使用@Controller时需要用@ResponseBody注释.
Spring 3.x 或使用@Controller情况下,在方法上使用@ResponseBody注释时,Spring会转换返回值并自动将其写入HTTP响应。Controller类中的每个方法都必须使用@ResponseBody进行注释。
@Controller
@RequestMapping("employees")
public class EmployeeController {
Employee employee = new Employee();
@RequestMapping(value = "/{name}", method = RequestMethod.GET, produces = "application/json")
public @ResponseBody Employee getEmployeeInJSON(@PathVariable String name) {
employee.setName(name);
employee.setEmail("[email protected]");
return employee;
}
@RequestMapping(value = "/{name}.xml", method = RequestMethod.GET, produces = "application/xml")
public @ResponseBody Employee getEmployeeInXML(@PathVariable String name) {
employee.setName(name);
employee.setEmail("[email protected]");
return employee;
}
}
2 @RestController自动添加@Controller和@ResponseBody注释
Spring 4.0引入了@RestController,这是一个控制器的专用版本,它是一个方便的注释,除了自动添加@Controller和@ResponseBody注释之外没有其他新魔法。
使用@RestController非常简单,这是从Spring v4.0开始创建MVC RESTful Web服务或基于SpringBoot 2的首选方法。
@RestController
public class EmployeeController {
Employee employee = new Employee();
@GetMapping("/employees/{name}")
public Employee getEmployeeInJSON(@PathVariable("name") String name) {
employee.setName(name);
employee.setEmail("[email protected]");
return employee;
}
}
#response统一格式
##一、本课程目标:
1. 弄清楚为什么要对springboot,所有Controller的response做统一格式封装?
1. 学会用ResponseBodyAdvice接口 和 @ControllerAdvice注解
##二、为什么要对springboot的接口返回值统一标准格式?
我们先来看下,springboot默认情况下的response是什么格式的
###第一种格式:response为String
```
@GetMapping(value="/getStr")
public String getStr( ){
return "test";
}
```
以上springboot的返回值为
```
test
```
###第二种格式:response为Objct
```
@GetMapping(value="/getObject")
public UserVO getObject( ){
UserVO vo=new UserVO();
vo.setUsername("agan");
return vo;
}
```
以上springboot的返回值为
```
{
"id": null,
"username": "agan",
"password": null,
"email": null,
"phone": null,
"idCard": null,
"sex": null,
"deleted": null,
"updateTime": null,
"createTime": null
}
```
###第三种格式:response为void
```
@GetMapping(value="/empty")
public void empty( ){
}
```
以上springboot的返回值为空
###第四种格式:response为异常
```
@GetMapping(value="/error")
public void error( ){
int i=9/0;
}
```
以上springboot的返回值为空
```
{
"timestamp": "2019-09-07T10:35:56.658+0000",
"status": 500,
"error": "Internal Server Error",
"message": "/ by zero",
"path": "/user/error"
}
```
以上3种,情况,如果你和客户端(app h5)开发人联调接口,他们会很懵逼,因为你给他们的接口没有一个统一的格式,客户端开发人员,不知道如何处理返回值。
故,我们应该统一response的标准格式。
##三、定义response的标准格式
一般的response的标准格式包含3部分:
1.status状态值:代表本次请求response的状态结果。
2.response描述:对本次状态码的描述。
3.data数据:本次返回的数据。
```
{
"status":0,
"desc":"成功",
"data":"test"
}
```
##四、初级程序员对response代码封装
对response的统一封装,是有一定的技术含量的,我们先来看下,初级程序员的封装,网上很多教程都是这么写的。
### 步骤1:把标准格式转换为代码
```
{
"status":0,
"desc":"成功",
"data":"test"
}
```
把以上格式转换为Result代码
```
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Result {
/**
* 1.status状态值:代表本次请求response的状态结果。
*/
private Integer status;
/**
* 2.response描述:对本次状态码的描述。
*/
private String desc;
/**
* 3.data数据:本次返回的数据。
*/
private T data;
/**
* 成功,创建ResResult:没data数据
*/
public static Result suc() {
Result result = new Result();
result.setResultCode(ResultCode.SUCCESS);
return result;
}
/**
* 成功,创建ResResult:有data数据
*/
public static Result suc(Object data) {
Result result = new Result();
result.setResultCode(ResultCode.SUCCESS);
result.setData(data);
return result;
}
/**
* 失败,指定status、desc
*/
public static Result fail(Integer status, String desc) {
Result result = new Result();
result.setStatus(status);
result.setDesc(desc);
return result;
}
/**
* 失败,指定ResultCode枚举
*/
public static Result fail(ResultCode resultCode) {
Result result = new Result();
result.setResultCode(resultCode);
return result;
}
/**
* 把ResultCode枚举转换为ResResult
*/
private void setResultCode(ResultCode code) {
this.status = code.code();
this.desc = code.message();
}
}
```
### 步骤2:把状态码存在枚举类里面
```
public enum ResultCode {
/* 成功状态码 */
SUCCESS(0, "成功"),
/* 系统500错误*/
SYSTEM_ERROR(10000, "系统异常,请稍后重试"),
UNAUTHORIZED(10401, "签名验证失败"),
/* 参数错误:10001-19999 */
PARAM_IS_INVALID(10001, "参数无效"),
/* 用户错误:20001-29999*/
USER_HAS_EXISTED(20001, "用户名已存在"),
USER_NOT_FIND(20002, "用户名不存在");
private Integer code;
private String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer code() {
return this.code;
}
public String message() {
return this.message;
}
}
```
### 步骤3:加一个体验类
```
@Api(description = "用户接口")
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@GetMapping(value="/getResult")
public Result getResult( ){
return Result.suc("test");
}
}
```
结论:看到这里,应该有很多同学都知道这样封装代码有很大弊端。
因为今后你每写一个接口,都要手工指定Result.suc()这行代码,多累啊??
如果你写这种代码推广给你整个公司用,然后硬性规定代码必须这么写!!所有程序都会吐槽鄙视!!!!
##五、高级程序员对response代码封装
如果你在公司推广你的编码规范,为了避免被公司其他程序员吐槽和鄙视,我们必须优化代码。
优化的目标:不要每个接口都手工指定Result返回值。
###步骤1:采用ResponseBodyAdvice技术来实现response的统一格式
springboot提供了ResponseBodyAdvice来帮我们处理
ResponseBodyAdvice的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来做response的统一格式、加解密、签名等等。
先看下ResponseBodyAdvice这个接口的源码。
```
public interface ResponseBodyAdvice {
/**
* 是否支持advice功能
* treu=支持,false=不支持
*/
boolean supports(MethodParameter var1, Class extends HttpMessageConverter>> var2);
/**
*
* 处理response的具体业务方法
*/
@Nullable
T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class extends HttpMessageConverter>> var4, ServerHttpRequest var5, ServerHttpResponse var6);
}
```
###步骤2:写一个ResponseBodyAdvice实现类
```
@ControllerAdvice(basePackages = "com.agan.boot")
public class ResponseHandler implements ResponseBodyAdvice
代码
@ControllerAdvice(basePackages = "com.agan.boot")
public class ResponseHandler implements ResponseBodyAdvice
以上代码,有2个地方需要重点讲解:
#### 第1个地方:@ControllerAdvice 注解:
@ControllerAdvice这是一个非常有用的注解,它的作用是增强Controller的扩展功能类。
那@ControllerAdvice对Controller增强了哪些扩展功能呢?主要体现在2方面:
1. 对Controller全局数据统一处理,例如,我们这节课就是对response统一封装。
1. 对Controller全局异常统一处理,这个后面的课程会详细讲解。
在使用@ControllerAdvice时,还要特别注意,加上basePackages,
@ControllerAdvice(basePackages = "com.agan.boot"),因为如果不加的话,它可是对整个系统的Controller做了扩展功能,
它会对某些特殊功能产生冲突,例如 不加的话,在使用swagger时会出现空白页异常。
#### 第2个地方:beforeBodyWrite方法体的response类型判断
以上代码一定要加,因为Controller的返回值为String的时候,它是直接返回String,不是json,
故我们要手工做下json转换处理
if (o instanceof String) {
return JsonUtil.object2Json(ResResult.suc(o));
}
##一、本课程目标:
1. 弄懂为什么springboot需要《全局异常处理器》?
2. 编码实战一个springboot《全局异常处理器》
3. 封装一个自定义异常 ,并集成进《局异常处理器》
4. 把《全局异常处理器》集成进《接口返回值统一标准格式》
## 二、springboot为什么需要全局异常处理器?
1. 先讲下什么是全局异常处理器?
全局异常处理器就是把整个系统的异常统一自动处理,程序员可以做到不用写try...catch
2. 那为什么需要全局异常呢?
- 第一个原因:不用强制写try-catch,由全局异常处理器统一捕获处理
```
@PostMapping(value="/error1")
public void error1( ){
int i=9/0;
}
```
如果不用try-catch捕获的话,客户端就会怎么样?
```
{
"timestamp": "2019-10-02T02:15:26.591+0000",
"status": 500,
"error": "Internal Server Error",
"message": "/ by zero",
"path": "/user/error1"
}
```
这种格式对于客户端来说,不友好,而一般程序员的try-catch
```
@PostMapping(value="/error11")
public String error11( ){
try{
int i=9/0;
}catch (Exception ex){
log.error("异常:{}",ex);
return "no";
}
return "ok";
}
```
但是还要一直自动化处理的,就是不用谢try-catch,由全局异常处理器来处理。
- 第二个原因:自定义异常,只能用全局异常来捕获。
```
@PostMapping(value="/error4")
public void error4( ){
throw new RuntimeException("用户已存在!!");
}
```
```
{
"timestamp": "2019-10-02T02:18:26.843+0000",
"status": 500,
"error": "Internal Server Error",
"message": "用户已存在!!",
"path": "/user/error4"
}
```
不可能这样直接返回给客户端,客户端是看不懂的,而且需要接入进《接口返回值统一标准格式》
- 第三个原因:JSR303规范的Validator参数校验器,参数校验不通过会抛异常,是无法使用try-catch语句直接捕获,
只能使用全局异常处理器了,JSR303规范的Validator参数校验器的异常处理后面课程会单独讲解,本节课暂不讲解。
## 三、案例实战:编码实现一个springboot《全局异常处理器》
### 步骤1:封装异常内容,统一存储在枚举类中
把所有的未知运行是异常都,用SYSTEM_ERROR(10000, "系统异常,请稍后重试")来提示
```
public enum ResultCode {
/* 成功状态码 */
SUCCESS(0, "成功"),
/* 系统500错误*/
SYSTEM_ERROR(10000, "系统异常,请稍后重试"),
UNAUTHORIZED(10401, "签名验证失败"),
/* 参数错误:10001-19999 */
PARAM_IS_INVALID(10001, "参数无效"),
/* 用户错误:20001-29999*/
USER_HAS_EXISTED(20001, "用户名已存在"),
USER_NOT_FIND(20002, "用户名不存在");
private Integer code;
private String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer code() {
return this.code;
}
public String message() {
return this.message;
}
}
```
### 步骤2:封装Controller的异常结果
最终目标格式:
```
{
"status": 10000,
"message": "系统异常,请稍后重试",
"exception": "java.lang.ArithmeticException"
}
```
```
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class ErrorResult {
/**
* 异常状态码
*/
private Integer status;
/**
* 用户看得见的异常,例如 用户名重复!!,
*/
private String message;
/**
* 异常的名字
*/
private String exception;
/**
* 异常堆栈信息
*/
//private String errors;
/**
* 对异常提示语进行封装
*/
public static ErrorResult fail(ResultCode resultCode, Throwable e,String message) {
ErrorResult result = ErrorResult.fail(resultCode, e);
result.setMessage(message);
return result;
}
/**
* 对异常枚举进行封装
*/
public static ErrorResult fail(ResultCode resultCode, Throwable e) {
ErrorResult result = new ErrorResult();
result.setMessage(resultCode.message());
result.setStatus(resultCode.code());
result.setException(e.getClass().getName());
//result.setErrors(Throwables.getStackTraceAsString(e));
return result;
}
}
```
### 步骤3:加个全局异常处理器,对异常进行处理
```
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理运行时异常
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Throwable.class)
public ErrorResult handleThrowable(Throwable e, HttpServletRequest request) {
//TODO 运行是异常,可以在这里记录,用于发异常邮件通知
ErrorResult error =ErrorResult.fail(ResultCode.SYSTEM_ERROR, e);
log.error("URL:{} ,系统异常: ",request.getRequestURI(), e);
return error;
}
}
```
handleThrowable方法的作用是:捕获运行时异常,并把异常统一封装为ErrorResult对象。
以上有几个细节点我们要单独讲解:
1. @RestControllerAdvice在《接口返回值统一标准格式》课程的时候,我们就讲解过它是增强Controller的扩展功能。而全局异常处理器,就是扩展功能之一。
2. @ExceptionHandler统一处理某一类异常,从而能够减少代码重复率和复杂度,@ExceptionHandler(Throwable.class)指处理Throwable的异常。
3. @ResponseStatus指定客户端收到的http状态码,这里配置500错误,客户端就显示500错误,
### 步骤4:体验效果
```
@PostMapping(value="/error1")
public void error1( ){
int i=9/0;
}
```
结果
```
{
"status": 10000,
"message": "系统异常,请稍后重试",
"exception": "java.lang.ArithmeticException"
}
```
## 三、案例实战:把自定义异常 集成 进《全局异常处理器》
### 步骤1:封装一个自定义异常
自定义异常通常是集成RuntimeException
```
@Data
public class BusinessException extends RuntimeException {
protected Integer code;
protected String message;
public BusinessException(ResultCode resultCode) {
this.code = resultCode.code();
this.message = resultCode.message();
}
}
```
### 步骤2:把自定义异常 集成 进全局异常处理器
全局异常处理器只要在上节课的基础上,添加一个自定义异常处理即可。
```
/**
* 处理自定义异常
*/
@ExceptionHandler(BusinessException.class)
public ErrorResult handleBusinessException(BusinessException e, HttpServletRequest request) {
ErrorResult error = ErrorResult.builder().status(e.code)
.message(e.message)
.exception(e.getClass().getName())
.build();
log.warn("URL:{} ,业务异常:{}", request.getRequestURI(),error);
return error;
}
```
### 步骤3:体验效果
```
@PostMapping(value="/error3")
public void error3( ){
throw new BusinessException(ResultCode.USER_HAS_EXISTED);
}
```
结果
```
{
"status": 20001,
"message": "用户名已存在",
"exception": "com.agan.boot.exceptions.BusinessException"
}
```
## 四、案例实战:把《全局异常处理器》集成进《接口返回值统一标准格式》
目标:把《全局异常处理器》的json格式转换为《接口返回值统一标准格式》格式
```
{
"status": 20001,
"message": "用户名已存在",
"exception": "com.agan.boot.exceptions.BusinessException"
}
```
转换
```
{
"status":20001,
"desc":"用户名已存在",
"data":null
}
```
### 步骤1:改造ResponseHandler
```
@ControllerAdvice(basePackages = "com.agan.boot")
public class ResponseHandler implements ResponseBodyAdvice
### 步骤3:加个全局异常处理器,对异常进行处理
handleThrowable方法的作用是:捕获运行时异常,并把异常统一封装为ErrorResult对象。
以上有几个细节点我们要单独讲解:
1. @RestControllerAdvice在《接口返回值统一标准格式》课程的时候,我们就讲解过它是增强Controller的扩展功能。而全局异常处理器,就是扩展功能之一。
2. @ExceptionHandler统一处理某一类异常,从而能够减少代码重复率和复杂度,@ExceptionHandler(Throwable.class)指处理Throwable的异常。
3. @ResponseStatus指定客户端收到的http状态码,这里配置500错误,客户端就显示500错误,
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理运行时异常
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Throwable.class)
public ErrorResult handleThrowable(Throwable e, HttpServletRequest request) {
//TODO 运行是异常,可以在这里记录,用于发异常邮件通知
ErrorResult error =ErrorResult.fail(ResultCode.SYSTEM_ERROR, e);
log.error("URL:{} ,系统异常: ",request.getRequestURI(), e);
return error;
}
}
##一、课程目标
1. 什么是Validator参数校验器? 为什么要用?
2. 编码实现对用户名、密码、邮箱、身份证的Validator校验
3. 实现一个手机号码的自定义校验
4. 把validator异常加入《全局异常处理器》
##二、 为什么要用Validator参数校验器,它解决了什么问题?
背景:在日常的接口开发中,经常要对接口的参数做校验,例如,登录的时候要校验用户名 密码是否为空。但是这种日常的接口参数校验太烦锁了,代码繁琐又多。
Validator框架就是为了解决开发人员在开发的时候少写代码,提升开发效率的;它专门用来做接口参数的校验的,例如 email校验、用户名长度必须位于6到12之间 等等。
原理:spring 的validator校验框架遵循了JSR-303验证规范(参数校验规范),JSR是Java Specification Requests的缩写。
在默认情况下,Spring Boot会引入Hibernate Validator机制来支持JSR-303验证规范。
spring boot的validator校验框架有3个特性:
1. JSR303特性: JSR303是一项标准,只提供规范不提供实现,规定一些校验规范即校验注解,如@Null,@NotNull,@Pattern,位于javax.validation.constraints包下。
2. hibernate validation特性:hibernate validation是对JSR303规范的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等
3. spring validation:spring validation对hibernate validation进行了二次封装,在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中。
##三、案例实战:实现一个SpringBoot的参数校验功能
### 步骤1:pom文件加入依赖包
springboot天然支持validator数据校验
```
org.springframework.boot
spring-boot-starter-web
```
### 步骤2:创建一个VO类
```
@Data
public class UserVO {
private Integer id;
@NotEmpty(message="用户名不能为空")
@Length(min=6,max = 12,message="用户名长度必须位于6到12之间")
private String username;
@NotEmpty(message="密码不能为空")
@Length(min=6,message="密码长度不能小于6位")
private String password;
@Email(message="请输入正确的邮箱")
private String email;
// @Phone
// private String phone;
@Pattern(regexp = "^(\\d{18,18}|\\d{15,15}|(\\d{17,17}[x|X]))$", message = "身份证格式错误")
private String idCard;
private Byte sex;
private Byte deleted;
private Date updateTime;
private Date createTime;
}
```
### 步骤3:加一个体验类
```
@Api(description = "用户接口")
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping(value = "/user/create", produces = APPLICATION_JSON_UTF8_VALUE, consumes = APPLICATION_JSON_UTF8_VALUE)
public void crreteUser(@RequestBody @Validated UserVO userVO) {
}
}
```
注意:以上代码一定要加上@Validated注解,它的作用是用来校验UserVO的参数是否正确
执行结果:
```
{
"timestamp": "2019-10-03T02:34:54.545+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"Pattern.userVO.idCard",
"Pattern.idCard",
"Pattern.java.lang.String",
"Pattern"
],
"arguments": [
{
"codes": [
"userVO.idCard",
"idCard"
],
"arguments": null,
"defaultMessage": "idCard",
"code": "idCard"
},
[],
{
"defaultMessage": "^(\\d{18,18}|\\d{15,15}|(\\d{17,17}[x|X]))$",
"arguments": null,
"codes": [
"^(\\d{18,18}|\\d{15,15}|(\\d{17,17}[x|X]))$"
]
}
],
"defaultMessage": "身份证格式错误",
"objectName": "userVO",
"field": "idCard",
"rejectedValue": "string",
"bindingFailure": false,
"code": "Pattern"
},
{
"codes": [
"Email.userVO.email",
"Email.email",
"Email.java.lang.String",
"Email"
],
"arguments": [
{
"codes": [
"userVO.email",
"email"
],
"arguments": null,
"defaultMessage": "email",
"code": "email"
},
[],
{
"defaultMessage": ".*",
"arguments": null,
"codes": [
".*"
]
}
],
"defaultMessage": "请输入正确的邮箱",
"objectName": "userVO",
"field": "email",
"rejectedValue": "string",
"bindingFailure": false,
"code": "Email"
}
],
"message": "Validation failed for object='userVO'. Error count: 2",
"path": "/user/user/create"
}
```
## 四、Validation常用注解
- @Null 限制只能为null
- @NotNull 限制必须不为null
- @AssertFalse 限制必须为false
- @AssertTrue 限制必须为true
- @DecimalMax(value) 限制必须为一个不大于指定值的数字
- @DecimalMin(value) 限制必须为一个不小于指定值的数字
- @Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
- @Future 限制必须是一个将来的日期
- @Max(value) 限制必须为一个不大于指定值的数字
- @Min(value) 限制必须为一个不小于指定值的数字
- @Past 限制必须是一个过去的日期
- @Pattern(value) 限制必须符合指定的正则表达式
- @Size(max,min) 限制字符长度必须在min到max之间
- @Past 验证注解的元素值(日期类型)比当前时间早
- @NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
- @NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
- @Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
##自定义validator注解
为什么要自定义validator注解呢?
因为validator框架支持的注解有限,不可能方方面面都支持,故需要我们自定义注解。
我们就以手机号码为例子,教大家如何写一个对手机号码校验的validator注解。
### 步骤1:创建一个@interface的手机校验注解
```
@Documented
// 指定注解的实现类
@Constraint(validatedBy = PhoneValidator.class)
@Target( { METHOD, FIELD })
@Retention(RUNTIME)
public @interface Phone {
String message() default "请输入正确的手机号码";
Class>[] groups() default { };
Class extends Payload>[] payload() default { };
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@interface List {
Phone[] value();
}
}
```
### 步骤2:手机号码校验注解实现类
```
public class PhoneValidator implements ConstraintValidator {
private static final Pattern PHONE_PATTERN = Pattern.compile(
"^((13[0-9])|(15[^4])|(18[0,2,3,5-9])|(17[0-8])|(147))\\d{8}$"
);
@Override
public void initialize(Phone constraintAnnotation) {
}
/**
*
* 校验的实现逻辑
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if ( value == null || value.length() == 0 ) {
return true;
}
Matcher m = PHONE_PATTERN.matcher(value);
return m.matches();
}
}
```
### 步骤3:给UserVO类,加上手机号码校验注解
```
@Data
public class UserVO {
private Integer id;
@NotEmpty(message="用户名不能为空")
@Length(min=6,max = 12,message="用户名长度必须位于6到12之间")
private String username;
@NotEmpty(message="密码不能为空")
@Length(min=6,message="密码长度不能小于6位")
private String password;
@Email(message="请输入正确的邮箱")
private String email;
@Phone
private String phone;
@Pattern(regexp = "^(\\d{18,18}|\\d{15,15}|(\\d{17,17}[x|X]))$", message = "身份证格式错误")
private String idCard;
private Byte sex;
private Byte deleted;
private Date updateTime;
private Date createTime;
}
```
### 步骤4:体验效果
```
{
"timestamp": "2019-10-03T03:42:10.928+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"Phone.userVO.phone",
"Phone.phone",
"Phone.java.lang.String",
"Phone"
],
"arguments": [
{
"codes": [
"userVO.phone",
"phone"
],
"arguments": null,
"defaultMessage": "phone",
"code": "phone"
}
],
"defaultMessage": "请输入正确的手机号码",
"objectName": "userVO",
"field": "phone",
"rejectedValue": "string",
"bindingFailure": false,
"code": "Phone"
}
],
"message": "Validation failed for object='userVO'. Error count: 1",
"path": "/user/user/create"
}
```
## 六、把validator异常加入《全局异常处理器》
那为什么要把validator异常加入《全局异常处理器》呢?
因为validator异常返回的内容是json,而且json数据结构(例如上文的json)特别复杂.
不利于客户端联调,而且也不友好提示,故,必须加入《全局异常处理器》
### 步骤1:《全局异常处理器》加入validator异常处理
```
/**
* validator 统一异常封装
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
String msgs = this.handle(e.getBindingResult().getFieldErrors());
ErrorResult error = ErrorResult.fail(ResultCode.PARAM_IS_INVALID, e, msgs);
log.warn("URL:{} ,参数校验异常:{}", request.getRequestURI(),msgs);
return error;
}
private String handle(List fieldErrors) {
StringBuilder sb = new StringBuilder();
for (FieldError obj : fieldErrors) {
sb.append(obj.getField());
sb.append("=[");
sb.append(obj.getDefaultMessage());
sb.append("] ");
}
return sb.toString();
}
```
### 步骤2:结果
```
{
"status": 10001,
"desc": "idCard=[身份证格式错误] email=[请输入正确的邮箱] phone=[请输入正确的手机号码] ",
"data": null
}
```
### 七:课后练习题
本课程的UserVO,还遗留了一个sex没有校验,请为该字段设计一个自定义注解;
注解的校验规则:0=女,1=男,输入错误提示:性别输入错误!
因为validator异常返回的内容是json,而且json数据结构(例如上文的json)特别复杂.
不利于客户端联调,而且也不友好提示,故,必须加入《全局异常处理器》
springboot安全:接口攻击
### 本课程目标:
面试的时候经常会被问到,如何保证你的接口完全?接口安全最常见的做法就是篡改和重放。
故,本节课我们就来学习:
1. 什么是接口篡改攻击?
2. 什么是重放攻击?
3. 编码实现SpringBoot的签名保护,防篡改重放攻击
### 一、为什么API接口是不安全的?
对于互联网来说,只要你系统的接口暴露在外网,就避免不了接口安全问题。
如果你的接口在外网裸奔,只要让黑客知道接口的地址和参数就可以调用,那简直就是灾难。
举个例子:你的网站用户注册的时候,需要填写手机号,发送手机验证码,如果这个发送验证码的接口没有经过特殊安全处理,那这个短信接口早就被人盗刷不知道浪费了多少钱了。
### 二、看看淘宝的API接口安全是怎么做的?
浏览器输入:https://h5.m.taobao.com/
然后过滤带有sign的URL
![image](https://github.com/agan-java/images/blob/master/sign/01.png?raw=true)
![image](https://github.com/agan-java/images/blob/master/sign/02.png?raw=true)
从上图可以看出,淘宝的接口统一加了t 和 sign 来实现接口安全的,先别急,接下来我们会一一讲解这2个参数的原理。
### 三、什么是接口篡改攻击?
接口篡改是指黑客通过http捉包的形式获取了你api接口的请求参数,然后篡改api参数内容,重新发送api请求。
例如上文说的,黑客通过http捉包破解了你的手机发送验证码接口,然后篡改手机号码,最终盗刷短信,浪费你的钱财。
### 四、接口如何实现请求内容防止篡改?
一般的做法有2种:
1. 采用https方式把接口的数据进行加密传输,即便是被黑客破解,黑客也花费大量的时间和精力去破解。
2. 接口后台对接口的请求参数进行验证,防止被黑客篡改;原理如下:
![image](https://github.com/agan-java/images/blob/master/sign/03.png?raw=true)
- 步骤1:服务端和客户端约定好加密规则,然后彼此都对api接口参数进行加密。
- 步骤2:客户端按步骤1,对api传输参数进行加密后得到签名值sign1,并将sign1,发送给服务端。
- 步骤3:服务端接收到客户端的请求后,按步骤1对请求参数进行加密,得到签名值sign2.
- 步骤4:服务端比对sign1和sign2的值,如果不一致,就认定为被篡改,非法请求。
以上2种方式,互联网公司都是同时采用的,例如淘宝。
### 五、什么是重放攻击?
重放攻击说白了就是黑客通过http捉包的形式,获取了你接口的请求参数(没篡改内容),然后重复发送请求。
重复攻击会造成2种严重后果:
1. 针对插入数据库接口:重放攻击,会出现大量重复数据,甚至垃圾数据会把数据库撑爆。
2. 针对查询的接口:黑客一般是重点攻击慢查询接口,例如一个慢查询接口1s,只要黑客发起重放攻击,就必然造成系统被拖垮,数据库查询被阻塞死。
### 六、接口如何实现请求内容防止重放?
业界的做法通常是基于timestamp的方案(淘宝也是这种方案)来防止重放。
原理如下:
![image](https://github.com/agan-java/images/blob/master/sign/04.png?raw=true)
- 步骤1:客户端每次发起http请求,而外新增timestamp参数,一起发生给服务端。
- 步骤2:服务端接收到http请求后,判断timestamp时间戳与当前时间是否操过60s(过期时间根据业务情况设置),如果超过了就提示签名过期。
也许有的同学会问,黑客每次请求完后再修改timestamp时间戳,不就破解你这个防止重放算法了吗
![image](https://github.com/agan-java/images/blob/master/sign/05.png?raw=true)
这确实,能被破解,那如何防止呢? 大家想一想,黑客修改我们的timestamp,是不是就是上文说的接口篡改,
所以采用上文的知识点,把timestamp和api参数一起进行加密后得到签名值sign1,并将sign1,发送给服务端;服务端再做验证就解决问题了。
/**
* 防篡改、防重放攻击过滤器
*/
@Slf4j
public class SignFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
log.info("初始化 signfilter.............");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filter) throws ServletException, IOException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
log.debug("过滤URL:{}", httpRequest.getRequestURI());
HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper(httpRequest);
String sign = httpRequest.getHeader("sign");
//验证sign不能为空
if (StringUtils.isEmpty(sign)) {
responseFail(httpResponse);
return;
}
//验证timestamp是否为空
String time = httpRequest.getHeader("timestamp");
if (StringUtils.isEmpty(time)) {
responseFail(httpResponse);
return;
}
/*
* 重放处理
* 判断timestamp时间戳与当前时间是否操过60s(过期时间根据业务情况设置),如果超过了就提示签名过期。
*/
long timestamp = Long.valueOf(time);
long now = System.currentTimeMillis() / 1000;
//为了方便测试这里改成1小时
long n = 60 * 60;
if (now - timestamp > n) {
responseFail(httpResponse);
return;
}
boolean accept = true;
SortedMap paramMap;
switch (httpRequest.getMethod()) {
case "GET":
paramMap = HttpDataUtil.getUrlParams(requestWrapper);
accept = SignUtil.verifySign(paramMap, sign, timestamp);
break;
case "POST":
case "PUT":
case "DELETE":
paramMap = HttpDataUtil.getBodyParams(requestWrapper);
accept = SignUtil.verifySign(paramMap, sign, timestamp);
break;
default:
accept = true;
break;
}
if (accept) {
filter.doFilter(requestWrapper, response);
} else {
responseFail(httpResponse);
return;
}
}
/**
* 异常返回
*/
private void responseFail(HttpServletResponse response) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
//抛出异常401 未授权状态码
response.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter out = response.getWriter();
//统一提示:签名验证失败
Result fail = Result.fail(ResultCode.UNAUTHORIZED);
String result = JsonUtil.object2Json(fail);
out.println(result);
out.flush();
out.close();
}
}