1.springboot常用配置文件
springboot推荐使用配置是yml格式的配置文件,当properties和yml文件共存的时候,properties的优先级更高,yml具体格式如下:
server:
port: 8080
spring:
redis:
port: 6379
host: 21.36.145.120
1.2.环境隔离
通常我们再开发中一般会有两种开发环境,一种是开发环境,一种是用户环境,yml中提供了一种语法格式来方便我们切换这两种环境
环境激活
#环境激活
spring:
profiles:
active: dev
通过新建两个新的配置文件,在主配置文件中可以自由切换其他两个配置文件
2.使用yml配置的方式注入属性值
使用yml配置的方式注入属性值好处就是直接在yml配置中设置属性值,避免了繁琐的设置,简化了属性值注入。
2.1.@ConfigurationProperties方式进行设置
如果想要使用这种方式需要用到两个注解:
@ConfigurationProperties(prefix = "ksd.weixin")@EnableConfigurationProperties(WeiXinPayProperties.class)
第一个注解是设置yml的具体格式,第二个注解是来告诉springboot需要加载我这个配置类,设置完成后如下:
ksd:
weixin:
appid: 1
需要说明,如果使用这种方式的话,属性值必须要有getter和setter方法,否则会注入不成功,并且也需要使用@Autowired进行自动注入,而且需要按顺序来进行注入
2.2.@value方式进行注入
格式如下
@Value("${ksd.weixin.appid}")
private String appid;
@value方式进行设置的话,需要在每个属性变量上面加上@value注解,并且填写上具体的格式
3.实现在yml中出现自定义的属性提示
把自定义属性添加到官方提示中去,可以方便我们的使用,使用这种方式需要我们导入一个依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
然后关闭所有yml文件,在maven种点击compile进行重新加载,再打开yml文件就可以出现自动提示
1.SpringBoot的日志解决方案
在springboot的底层日志结构中对应:spring-boot-starter-logging
可以看出,它依赖了三个框架分别是:
-
slf4j
提供了java所有的日志框架的简单抽象(使用了日志的门面设计模式),说白了就是一个日志API(
没有实现类
), 它不能单独使用,必须要结logback和log4j日志框架来实现结合使用。 -
logback
-
log4j
2.日志搭配
springboot2.x以后默认采用了:slf4j+logback的日志搭配。但是我们也可以通过配置来使用其他的日志方案
package com.kuangstudy.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @description: * @author: xuke * @time: 2021/6/1 19:58 */ @RestController public class LogController { private static final Logger log = LoggerFactory.getLogger(LogController.class); @GetMapping("/log") public void console(){ log.trace("----------trace--------"); log.debug("----------debug--------"); log.info("----------info--------"); log.warn("----------warn--------"); log.error("----------error--------"); } }
代码中最重要是要是:
private static final Logger log = LoggerFactory.getLogger(LogController.class);
这段代码是导入log的配置类,并且初始化一个配置对象,并且参数是你要加载的Java文件
springboot默认的打印级别为info(trace>debug>info>warn>error)
3.日志级别的修改
在yml文件中可以设置打印级别的设置,格式如下
# 指定日志级别 把springboot的所有日志修改成为debug
logging:
level:
root: debug
在root后面就可以修改配置日志级别,如果想要只修改一个包下文件的日志打印级别就需要如下的配置
# 指定日志级别 把springboot的所有日志修改成为debug
logging:
level:
com:
kuangstudy: debug #kuangstudy是包名,后面的就是日志级别
4.日志的使用
日志的使用
package com.kuangstudy.controller;
import com.kuangstudy.entity.Course;
import com.kuangstudy.entity.User;
import com.kuangstudy.service.WeixinPayService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @description:
* @author: xuke
* @time: 2021/6/20 20:22
*/
@RestController
public class IndexController {
// 初始化一个日志对象
private static Logger log = LoggerFactory.getLogger(IndexController.class);
@Autowired
private WeixinPayService weixinPayService;
@GetMapping("/logs")
public String consolelogs() {
User user = new User();
user.setId(1);
Course course = new Course();
course.setCourseid(100);
course.setTitle("学相伴秋招课程班");
course.setPrice("1999");
weixinPayService.paycourse(user, course);
return "success";
}
}
package com.kuangstudy.service;
import com.kuangstudy.config.WeixinPayProperties;
import com.kuangstudy.entity.Course;
import com.kuangstudy.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class WeixinPayService {
private static Logger log = LoggerFactory.getLogger(WeixinPayService.class);
@Autowired
private WeixinPayProperties weixinPayProperties;
public void paycourse(User user, Course course) {
try {
log.info("当前支付的用户是:{},支付的课程是:{},金额是:{}", user.getId(), course.getTitle(), course.getPrice());
log.info("支付的appid是:{},回调地址是:{}", weixinPayProperties.getAppid(), weixinPayProperties.getCallbackurl());
} catch (Exception ex) {
log.error("支付出异常,异常信息是:{}", ex.getMessage());
}
}
}
log.info("当前支付的用户是:{},支付的课程是:{},金额是:{}", user.getId(), course.getTitle(), course.getPrice());
log.info("支付的appid是:{},回调地址是:{}",weixinPayProperties.getAppid(),weixinPayProperties.getCallbackurl());
在日志打印中可以自己设置格式来打印出自己想要的内容,比如,上面参数获得值就会填充到{}中,在控制台显示出来
5.lombok优化日志
需要导入Lombok的包和插件。
在lombok中提供了两个日志注解的支持:@Slf4j
和@Log4j2
建议在开发过程中就默认选择:@Slf4j
使用@Slf4j注解,就相当于添加了private static Logger log = LoggerFactory.getLogger(IndexController.class);
所以这个注解就是简化了代码
6.开发中的日志隔离
跟配置文件一样,日志设置同样有隔离设置,可以分为开发环境和生产环境
开发环境 application-dev.yml
开发环境:一般就在控制台打印即可,写文件着实没有任何意义。 日志级别是:debug
# 指定日志级别 把springboot的所有日志修改成为debug
logging:
level:
root: debug
pattern:
# console是控制台的日志的格式
console: "【KuangStudy-console】%d{yyyy/MM/dd-HH:mm:ss} [%thread] %-5level %logger{50} --%M- %msg%n"
生产环境 application-prod.yml
生产环境因为要不间断运行,所以日志我们很难看到,需要打印到具体文件中,方便我们查看
server:
port: 80
# 指定日志级别 把springboot的所有日志修改成为debug
logging:
level:
root: error
file:
# 如果不想把日志存放在logging.file.path目录下,可以采用name来重新定义存储的位置和日志文件的名称
name: /www/logs/kuangstudypro.log
pattern:
# file 是指日志文件中日志的格式
file: "【KuangStudy-file】%d{yyyy/MM/dd-HH:mm:ss} -- [%thread] %-5level %logger{50} -- %M - %msg%n"
还有一种使用xml文件配置的方式,logback.xml,此处留到以后讨论
1.swagger的用处
在开发中,现在主流开发方式都前后端分离的开放方式,面临这一个问题,如何校验接口的有效性。就必须采用一些工具:比如:postman
、swagger
和 小幺鸡
等。
简单来说就是生成一个接口文档,里面包含了你的写的函数和接口信息,比如参数列表之类,还可以提供简单的测试
2.swagger的使用
(1)导入包
<!-- Swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- 文档 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.8.5</version>
</dependency>
(2)编写配置类
/**
* itbooking系统平台<br/>
* com.itbooking.config<br/>
* SweggerConfiguration.java<br/>
* 创建人:mofeng <br/>
* 时间:2018年9月24日-下午5:35:07 <br/>
* 2018itbooking-版权所有<br/>
*/
package com.kuangstudy.config;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author 飞哥
* @Title: 学相伴出品
* @Description: 我们有一个学习网站:https://www.kuangstudy.com
* @date 2021/5/20 13:16
*/
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
/**
* 在完成上述配置之后,其实就已经可以产生帮助文档了,但是这样的文档主要针对请求本身,而描述主要来源于函数等命名产生。
* 对用户体验不好,我们通常需要自己增加一些说明来丰富文档内容。如果:
* 加入
*
* @ApiIgnore 忽略暴露的 api
* @ApiOperation(value = "查找", notes = "根据用户 ID 查找用户")
* 添加说明
* <p>
* <p>
* 其他注解:
* @Api :用在类上,说明该类的作用
* @ApiImplicitParams :用在方法上包含一组参数说明
* @ApiResponses :用于表示一组响应
* 完成上述之后,启动springboot程序,
* 旧访问:http://localhost:8080/swagger-ui.html
* 新访问:http://localhost:8080/doc.html
* @ApiOperation() 用于方法;表示一个http请求的操作
* value用于方法描述
* notes用于提示内容
* tags可以重新分组(视情况而用)
* @ApiParam() 用于方法,参数,字段说明;表示对参数的添加元数据(说明或是否必填等)
* name–参数名
* value–参数说明
* required–是否必填
* @ApiModel()用于类 ;表示对类进行说明,用于参数用实体类接收
* value–表示对象名
* description–描述
* 都可省略
* @ApiModelProperty()用于方法,字段; 表示对model属性的说明或者数据操作更改
* value–字段说明
* name–重写属性名字
* dataType–重写属性类型
* required–是否必填
* example–举例说明
* hidden–隐藏
* @ApiIgnore()用于类或者方法上,可以不被swagger显示在页面上 比较简单, 这里不做举例
* @ApiImplicitParam() 用于方法
* 表示单独的请求参数
* @ApiImplicitParams() 用于方法,包含多个 @ApiImplicitParam
* name–参数ming
* value–参数说明
* dataType–数据类型
* paramType–参数类型
* example–举例说明
*/
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(getApiInfo())
.select()
// 核心:读取把那个包下面的方法作为接口,只能是:controller
.apis(RequestHandlerSelectors.basePackage("com.kuangstudy.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo getApiInfo() {
return new ApiInfoBuilder()
.title("学相伴APP项目数据接口")
.description("学相伴APP项目数据接口,在线体验文档")
.termsOfServiceUrl("https://api.kuangstudy.com/api")
.contact("徐柯,阿超,狂神")
.version("1.0")
.build();
}
}
.apis(RequestHandlerSelectors.basePackage(“com.kuangstudy.controller”))
这个要注意,这个位置就是要写你自己的想要配置的包名
在上面导入的配置文件中还有一个包,里面是一种用bootstrap写的swagger新页面
旧业面访问地址:http://localhost:你服务器端口/swagger-ui.html
新页面访问地址:http://localhost:你服务器端口/doc.html
3.注解的使用和认识
package com.kuangstudy.entity;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import lombok.experimental.Accessors;
/**
* @description:
* @author: xuke
* @time: 2021/6/20 22:02
*/
@Data // 替代getter/setter
@ToString // 替代toString
@AllArgsConstructor // 有参数的构造函数
@NoArgsConstructor // 无参数的构造函数
@Accessors(chain = true)
@ApiModel(description = "用户实体")
public class User {
// 用户编号
@ApiModelProperty(value = "用户编号",required=true)
private Integer id;
// 用户昵称
@ApiModelProperty(value = "用户昵称",required=true)
private String nickname;
// 用户密码
@ApiModelProperty(value = "用户密码",required=true)
private String password;
// 用户头像
@ApiModelProperty(value = "用户头像",required=true)
private String avatar;
// 用户地址
@ApiModelProperty(value = "用户地址",required=true)
private String address;
}
package com.kuangstudy.controller;
import com.kuangstudy.entity.Course;
import com.kuangstudy.entity.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.annotations.ApiIgnore;
/**
* @description:
* @author: xuke
* @time: 2021/6/20 20:22
*/
@RestController
@Slf4j
@Api(description = "用户管理")
public class UserController {
@GetMapping("/user/save")
@ApiOperation(value = "用户注册")
@ApiImplicitParams(
@ApiImplicitParam(name = "user", value = "用户对象")
)
public String coursesave(User user) {
return "success";
}
}
1.应用场景
在SpringBoot的日常开发中,一般都是同步调用
的,但经常有特殊业务需要做异步
来处理。比如:注册用户、需要送积分、发短信和邮件、或者下单成功、发送消息等等。
比如注册用户是很重要的任务,我们可以使用主进程来去解决它,但是其他的例如积分发送、发送短信等任务跟主任务关联性不大,允许它们执行慢一些,甚至执行不成功留到下次继续执行都没问题,所以这些任务可以开启另外的线程来进行执行,放在主线程里面,容易造成主线程堵塞,导致注册用户这个大任务完成不了,并且也能加快任务完成时间
-
优点:
-
1.主任务和分任务各自独立完成,不会造成主任务的堵塞
-
2.加快主任务的完成时间,缩短总体完成时间
串行和并行
并行概念
串行执行的时长:是所有方法执行的总和、打个比方:用户注册:50MS 短信发送:100ms 、添加积分:100ms 总时长:250ms 这个方法执行完毕。
串行执行:所有的任务放在一个线程里面进行执行,只有线程中所有的任务全部成功执行结束,才算执行成功,否则全部执行失败异步概念
异步执行就是重现开启多个线程,让这些开启的线程去执行那些不怎么重要的任务,而主线程专心的去执行用户注册这一重要的任务,分析:它执行用户注册的执行时长,并不会因为短信发送、添加积分收到影响。执行时间:>50ms
异步执行:相当于百米冲刺,百米冲刺:最终的比赛结束一定最后一个到达终点的时间。
-
异步编程的步骤
1. 让springboot框架支持异步处理
package com.kuangstudy;
import com.kuangstudy.config.WeixinPayProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableAsync;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@SpringBootApplication
@EnableAsync //开启异步执行
public class KuangstudySpringbootProApplication {
public static void main(String[] args) {
SpringApplication.run(KuangstudySpringbootProApplication.class, args);
}
}
@EnableAsync //开启异步执行
加上这个注解就可以开启框架支持
2. 编写处理异步任务的service类
package com.kuangstudy.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
/**
* @description:
* @author: xuke
* @time: 2021/6/23 20:37
*/
@Service
@Slf4j
public class RegService {
// 发送短信,用异步进行处理和标记
@Async
public void sendMsg(){
// todo :模拟耗时5秒
try {
Thread.sleep(5000);
log.info("---------------发送消息--------");
}catch (Exception ex){
ex.printStackTrace();
}
}
// 添加积分,用异步进行处理和标记
@Async
public void addScore(){
// todo :模拟耗时5秒
try {
Thread.sleep(5000);
log.info("---------------处理积分--------");
}catch (Exception ex){
ex.printStackTrace();
}
}
}
@Async
这里也要加上注解,表示这个要开启一个新线程来执行
3. 调用异步处理
package com.kuangstudy.controller;
import com.kuangstudy.service.RegService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @description:
* @author: xuke
* @time: 2021/6/23 20:10
*/
@RestController
@Slf4j
public class RegController {
@Autowired
private RegService regService;
@GetMapping("/reg")
public String reguser(){
// 1: 注册用户 10ms
log.info("新用户注册");
//userService.save(user);
// 2: 发送短信 5s
log.info("发送短信");
regService.sendMsg();
// 3: 添加积分 5s
log.info("添加积分");
regService.addScore();
return "ok";
}
}
直接调用方法就可以开启一个异步执行
小结
在SpringBoot的日常开发中,一般都是同步调用的,但经常有特殊业务需要做异步来处理。比如:注册用户、需要送积分、发短信和邮件、或者下单成功、发送消息等等。
- 第一个原因:容错问题,如果送积分出现异常,不能因为送积分而导致用户注册失败。
- 第二个原因:提升性能,比如注册用户花了30毫秒,送积分划分50毫秒,如果同步的话一共耗时:70毫秒,用异步的话,无需等待积分,故耗时是:30毫秒就完成了业务。
异步线程池优化
Springboot的tomcat的线程默认数量:200个,如果异步线程线程过多,有请求线程、异步处理的线程这个时候,这么线程都在争抢CPU的执行时间。这样很耗费资源 ,因为@Async注解默认情况下用的是SimpleAsyncTaskExecutor
线程池.【该线程池不是真正意义上的线程】
Tomcat的线程是不能重用的,就导致,每执行一遍就会重新开启一个新线程来去执行,这样会加大资源消耗,所以需要线程池来去优化这个问题
@Async注解异步框架提供多种线程机制:
-
SimpleAsyncTaskExecutor:简单的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
-
SyncTaskExecutor:这个类没实现异步调用,只是一个同步操作,只适合用于不需要多线程的地方。
-
ConcurrentTaskExecutor:Executor的适配类,不推荐使用.。
-
ThreadPoolTaskScheduler:可以和cron表达式使用。
-
ThreadPoolTaskExecutor:最常用,推荐,其本质就是:java.util.concurrent.ThreadPoolExecutor的包装
需要添加一个名为SyncThreadPoolConfiguration
的Java文件package com.kuangstudy.config; import org.springframework.boot.SpringBootConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.ThreadPoolExecutor; /** * @description: * @author: xuke * @time: 2021/6/1 21:32 */ @Configuration public class SyncThreadPoolConfiguration { /** * 把springboot中的默认的异步线程线程池给覆盖掉。用ThreadPoolTaskExecutor来进行处理 **/ @Bean(name="threadPoolTaskExecutor") public ThreadPoolTaskExecutor getThreadPoolTaskExecutor(){ ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); // 1: 创建核心线程数 cpu核数 -- 50 threadPoolTaskExecutor.setCorePoolSize(10); // 2:线程池维护线程的最大数量,只有在缓存队列满了之后才会申请超过核心线程数的线程 threadPoolTaskExecutor.setMaxPoolSize(100); // 3:缓存队列 可以写大一点无非就浪费一点内存空间 threadPoolTaskExecutor.setQueueCapacity(200); // 4:线程的空闲事件,当超过了核心线程数之外的线程在达到指定的空闲时间会被销毁 200ms threadPoolTaskExecutor.setKeepAliveSeconds(200); // 5:异步方法内部线的名称 threadPoolTaskExecutor.setThreadNamePrefix("ksdsysn-thread-"); // 6:缓存队列的策略 多线程 JUC并发 /* 当线程的任务缓存队列已满并且线程池中的线程数量已经达到了最大连接数,如果还有任务来就会采取拒绝策略, * 通常有四种策略: *ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出异常:RejectedExcutionException异常 *ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常 *ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) *ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用execute()方法,直到成功。 *ThreadPoolExecutor. 扩展重试3次,如果3次都不充公在移除。 *jmeter 压力测试 1s=500 * */ threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); threadPoolTaskExecutor.initialize(); return threadPoolTaskExecutor; } }
异步的主要应用场景
当子任务与主任务关联不大的时候,并且不能因为子任务失败而导致主任务失败,这个时候就可以使用异步编程
- 异步编程的框架:消息中间件(ActiveMQ、RabbitMQ)
提出问题
当我们使用swagger来生成文档时,如果开发人员众多,每个人的代码风格都不尽相同,就会导致每个人写的返回值不同,会带来调试中的问题,所以就开始有人尝试用一套相同的格式来去替代返回值,这样返回值风格的统一有利于开发的进程
格式
# 成功的状态
{
code:200,
data:{id:"1",name:"yykkk"},
message:"success"
}
# 失败
{
code:401,
data:"",
message:"用户名和账号有误"
}
{
code:500,
data:"",
message:"服务器出错!!"
}
如何实现
封装统一返回的R类
把需要返回的值,同意封装到一个类里面,在返回值的时候就返回这个类,类中包含了我们所需要的所有信息
R类封装如下
package com.kuangstudy.common;
import lombok.Data;
/**
* # 成功的状态
* {
* code:200,
* data:{id:"1",name:"yykkk"},
* message:"success"
* }
*
* # 失败
* {
* code:401,
* data:"",
* message:"用户名和账号有误"
* }
*
* {
* code:500,
* data:"",
* message:"服务器出错!!"
* }
**/
@Data
public class R {
// 返回的编号
private Integer code;
// 返回的数据,数据类型N中,
private Object data;
// 返回的信息
private String message;
}
具体使用:
package com.kuangstudy.controller;
import com.kuangstudy.common.R;
import com.kuangstudy.service.RegService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class RegController {
@Autowired
private RegService regService;
@GetMapping("/reg")
public R reguser(){
// 1: 注册用户 10ms
log.info("新用户注册");
//userService.save(user);
R r = new R();
r.setCode(200);
r.setData("ok");
r.setMessage("注册成功!");
return r;
}
}
R类优化
使用静态方法封装来优化R类
R类中成功情况的封装
package com.kuangstudy.common;
import lombok.Data;
/**
* # 成功的状态
* {
* code:200,
* data:{id:"1",name:"yykkk"},
* message:"success"
* }
* <p>
* # 失败
* {
* code:401,
* data:"",
* message:"用户名和账号有误"
* }
* <p>
* {
* code:500,
* data:"",
* message:"服务器出错!!"
* }
**/
@Data
public class R {
// 返回的编号
private Integer code;
// 返回的数据,数据类型N中,
private Object data;
// 返回的信息
private String message;
public static R success(Object data, String message) {
/*这样只需要使用类名就可以调用方法,省去频繁创建类对象的烦恼*/
R r = new R();
r.setCode(200);
r.setData(data);
r.setMessage(message);
return r;
}
public static R success(Object data) {//这里用了函数重载,去掉了message参数
return success(data, "");
}
}
这样解决了代码的臃肿和冗余
R类中失败情况的封装
package com.kuangstudy.common;
/**
* @description:
* @author: xuke
* @time: 2021/6/23 21:47
*/
import lombok.Data;
@Data
public class R {
// 返回的编号
private Integer code;
// 返回的数据,数据类型N中,
private Object data;
// 返回的信息
private String message;
public static R success(Object data, String message) {
R r = new R();
r.setCode(200);
r.setData(data);
r.setMessage(message);
return r;
}
public static R success(Object data) {
return success(data, "");
}
//-------------------------------------------------------------------------
public static R fail(Integer code, String message) {
R r = new R();
r.setCode(code);
r.setData(null);
r.setMessage(message);
return r;
}
}
R类构造函数私有化
为什么要要构造函数私有化呢?
答案:当调用的过程变得单一,只允许用用类去调用方法,不允许类外用new去调用。
private R (){
}//这样就可以了,防止类外面调用
统一返回状态的维护和message的维护问题
if (flag.equals(1)) {
return R.fail(401, "用户名和密码错误!!!");
}
if (flag.equals(2)) {
return R.fail(402, "密码和确认密码不一致!!!");
}
在实际开发中,这种写法,在项目不大的时候可以这样写,但是当项目越来越大这样写就显得太不专业了,我们可以吧"用户名和密码错误!!!"
用常量来表示,在使用的时候直接调用就行了
集中管理的解决方案
package com.kuangstudy.common;
public class RConstants {
// 用户名和密码错误信息和状态
public static final Integer USER_REG_USER_PASSWORD_CODE = 401;
public static final String USER_REG_USER_PASSWORD_ERROR = "用户名和密码错误!";
// 密码和确认密码错误信息和状态
public static final Integer USER_REG_USER_PASSWORD_CONFIRM_CODE = 402;
public static final String USER_REG_USER_PASSWORD_CONFIRM_ERROR = "密码和确认密码不一致!";
}
if (flag.equals(1)) {
return R.fail(RConstants.USER_REG_USER_PASSWORD_CODE,RConstants.USER_REG_USER_PASSWORD_ERROR);
}
if (flag.equals(2)) {
return R.fail(RConstants.USER_REG_USER_PASSWORD_CONFIRM_CODE,RConstants.USER_REG_USER_PASSWORD_CONFIRM_ERROR);
}
这样就显得美观,也易于维护,但是这样就又引出一个问题,当常量定义多了以后,就会非常难以维护,毕竟查找就要找半天,这个时候就要用到枚举类了
具体实现
package com.kuangstudy.common;
/**
* @description: 统一返回的常量类
* 对内修改开放,对外修改关闭---枚举
* @author: xuke
* @time: 2021/6/23 22:12
* 枚举本质是一个Java类,他的构造函数不需要访问修饰符,因为它不需要让外部来调用
*/
public enum ResponseEnum {
USER_REG_USER_PASSWORD_CODE(401,"用户名和密码错误"),
USER_REG_USER_PASSWORD_CONFIRM(402,"密码和确认密码不一致");
private Integer code;
private String message;
ResponseEnum(Integer code,String mesage){
this.code = code;
this.message =mesage;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
使用效果如下:
if (flag.equals(1)) {
return R.fail(ResponseEnum.USER_REG_USER_PASSWORD_CODE.getCode(),
ResponseEnum.USER_REG_USER_PASSWORD_CODE.getMessage());
}
if (flag.equals(2)) {
return R.fail(ResponseEnum.USER_REG_USER_PASSWORD_CONFIRM.getCode(),
ResponseEnum.USER_REG_USER_PASSWORD_CONFIRM.getMessage());
}
R类统一枚举的最终方案
package com.kuangstudy.common;
/**
* @description:
* @author: xuke
* @time: 2021/6/23 21:47
*/
import lombok.Data;
@Data
public class R {
// 返回的编号
private Integer code;
// 返回的数据,数据类型N中,
private Object data;
// 返回的信息
private String message;
private R() {
}
public static R success(Object data, String message) {
R r = new R();
r.setCode(ResponseEnum.SUCCESS.getCode());
r.setData(data);
r.setMessage(message == null ? ResponseEnum.SUCCESS.getMessage() : message);
return r;
}
public static R success(Object data) {
return success(data, null);
}
public static R fail(Integer code, String message) {
R r = new R();
r.setCode(code);
r.setData(null);
r.setMessage(message);
return r;
}
public static R fail(ResponseEnum responseEnum) {
R r = new R();
r.setCode(responseEnum.getCode());
r.setData(null);
r.setMessage(responseEnum.getMessage());
return r;
}
}
枚举类的定义
package com.kuangstudy.common;
/**
* @description: 统一返回的常量类
* 对内修改开放,对外修改关闭---枚举
* @author: xuke
* @time: 2021/6/23 22:12
*/
public enum ResponseEnum {
SUCCESS(200,"成功!"),
USER_REG_USER_PASSWORD_CODE(401,"用户名和密码错误"),
USER_REG_USER_PASSWORD_CONFIRM(402,"密码和确认密码不一致"),
ORDER_FAIL(601,"订单失败"),
ORDER_MESSAGE_FAIL(602,"订单发送消息失败") ;
private Integer code;
private String message;
ResponseEnum(Integer code,String mesage){
this.code = code;
this.message =mesage;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
调用测试:
/**
* @return java.lang.String
* @Author xuke
* @Description 用户注册
* @Date 20:11 2021/6/23
* @Param []
**/
@GetMapping("/reg2")
public R reguser2(Integer flag) {
if (flag.equals(1)) {
return R.fail(ResponseEnum.USER_REG_USER_PASSWORD_CODE);
}
if (flag.equals(2)) {
return R.fail(ResponseEnum.USER_REG_USER_PASSWORD_CONFIRM);
}
// 1: 注册用户 10ms
log.info("新用户注册");
//userService.save(user);
// 2: 发送短信 5s
log.info("发送短信");
regService.sendMsg();
// 3: 添加积分 5s
log.info("添加积分");
regService.addScore();
return R.success("ok");
}
为什么Springboot需要《全局异常处理》
全局异常处理就是把整个程序中出现的异常收集起来做集中处理
原因:
- 不用强制写try/catch,异常交由统一异常的处理机制进行捕获。
在开发中,如果不用try/catch进行捕获的话。客户端就会跳转到springboot默认的异常页面。报出500的错误信息。
在开发中遇见了异常一般的程序开发者都会使用try/catch来进行捕获处理 - 可以自定义异常来去处理我们的错误,能够更直观的展示错误,方便定位
- 第三个原因:JSR303的参数验证器,参数校验不通过会抛出异常,也是无法通过try/catch进行直接捕获处理的。
具体实现步骤
1. 定义全局异常处理枚举类
作用:这个枚举类里面包含了所有的异常信息,为自定义异常提供了一个场所,使用枚举类也跟方便获取信息和定义异常
package com.kuangstudy.exception;
import lombok.Getter;
@Getter
public enum ResultCodeEnum {
UNKNOWN_REASON(false, 20001, "未知错误"),
SERVER_ERROR(false, 500, "服务器忙,请稍后在试"),
ORDER_CREATE_FAIL(false, 601, "订单下单失败");
private Boolean success;
private Integer code;
private String message;
ResultCodeEnum(Boolean success, Integer code, String message) {
this.success = success;
this.code = code;
this.message = message;
}
}
2. 封装controller中出现的异常
package com.kuangstudy.exception;
import com.kuangstudy.common.R;
import lombok.*;
/**
* @author 飞哥
* @Title: 学相伴出品
* @Description: 我们有一个学习网站:https://www.kuangstudy.com
* @date 2021/6/2 10:32
*/
@Builder
@AllArgsConstructor
#### #### @NoArgsConstructor
@Data
@ToString
public class ErrorHandler {
// ErrorHandler === R 答案:不想破坏R类。
// 异常的状态码,从枚举中获得
private Integer status;
// 异常的消息,写用户看得懂的异常,从枚举中得到
private String message;
// 异常的名字
private String exception;
public static ErrorHandler fail(ResultCodeEnum resultCodeEnum, Throwable throwable, String message) {
ErrorHandler errorHandler = ErrorHandler.fail(resultCodeEnum, throwable);
errorHandler.setMessage(message);
return errorHandler;
}
public static ErrorHandler fail(ResultCodeEnum resultCodeEnum, Throwable throwable) {
ErrorHandler errorHandler = new ErrorHandler();
errorHandler.setMessage(resultCodeEnum.getMessage());
errorHandler.setStatus(resultCodeEnum.getCode());
errorHandler.setException(throwable.getClass().getName());
return errorHandler;
}
}
3. 定义全局异常处理器
package com.kuangstudy.config.handler;
import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.ResultCodeEnum;
import com.kuangstudy.config.exception.BusinessException;
import com.kuangstudy.config.exception.OrderException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* @author 飞哥
* @Title: 学相伴出品
* @Description: 我们有一个学习网站:https://www.kuangstudy.com
* @date 2021/6/2 10:40
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 对服务器端出现500异常进行统一处理
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Throwable.class)
public ErrorHandler makeExcepton(Throwable e, HttpServletRequest request) {
ErrorHandler errorHandler = ErrorHandler.fail(ResultCodeEnum.SERVER_ERROR, e);
log.error("请求的地址是:{},出现的异常是:{}", request.getRequestURL(), e);
return errorHandler;
}
}
@RestControllerAdvice
和@ControllerAdvice
是对controller的增强扩展处理,而全局异常就是一种扩展能力之一。
@ControllerAdvice是让conroller中所有返回的值都会被拦截到,@RestControllerAdvice是让conroller中所有的异常都会被拦截进行统一处理@ExceptionHandler(Throwable.class)
:统一处理某一类型异常,从而减少代码的出现异常的复杂度和重复率.@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
:指定客户端收到的http状态码,这里配置的是500,就显示成500错误。不指定也是没问题的。因为返回是根据自己的枚举进行处理了。
```java
package com.kuangstudy.controller;
import com.kuangstudy.entity.User;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
- @description:
- @author: xuke
- @time: 2021/6/25 20:16
*/
@RestController
@Api(description = “用户中心”)
public class UserController {
@GetMapping("/error1")
public User error1(Integer id) {
if (id.equals(1)) {
throw new RuntimeException("用户名和密码有误!!!");
}
User user = new User();
user.setId(1);
user.setNickname("yykk");
user.setPassword("451212");
user.setAddress("梅州");
return user;
}
@GetMapping("/error2")
public User error2(Integer id) {
int i = 1 / 0;
User user = new User();
user.setId(1);
user.setNickname("yykk");
user.setPassword("451212");
user.setAddress("梅州");
return user;
}
@GetMapping("/getuser")
public User getuser(Integer id) {
User user = new User();
user.setId(1);
user.setNickname("yykk");
user.setPassword("451212");
user.setAddress("梅州");
return user;
}
@ResponseBody //---标记--jackson A 有 B没有
@GetMapping("/getname")
public String getusername() {
return "yykk";
}
## 自定义异常,并集成自定义异常处理器
### 1. 添加自定义异常
```java
package com.kuangstudy.config.exception;
import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.ResultCodeEnum;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* @author 飞哥
* @Title: 学相伴出品
* @Description: 我们有一个学习网站:https://www.kuangstudy.com
* @date 2021/6/2 10:40
*/
@Data
public class BusinessException extends RuntimeException {
private Integer code;
private String message;
public BusinessException(ResultCodeEnum resultCodeEnum) {
this.code = resultCodeEnum.getCode();
this.message = resultCodeEnum.getMessage();
}
public BusinessException(Integer code, String message) {
this.code = code;
this.message = message;
}
}
这个类继承了RuntimeException这个异常类,可以捕获运行时异常
2. 添加自定义异常处理方法
package com.kuangstudy.config.handler;
import com.kuangstudy.common.base.ErrorHandler;
import com.kuangstudy.common.base.ResultCodeEnum;
import com.kuangstudy.config.exception.BusinessException;
import com.kuangstudy.config.exception.OrderException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* @author 飞哥
* @Title: 学相伴出品
* @Description: 我们有一个学习网站:https://www.kuangstudy.com
* @date 2021/6/2 10:40
*/
@RestControllerAdvice(basePackages = "com.gong")
@Slf4j
public class GlobalExceptionHandler {
/**
* 对服务器端出现500异常进行统一处理
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Throwable.class)
public ErrorHandler makeExcepton(Throwable e, HttpServletRequest request) {
ErrorHandler errorHandler = ErrorHandler.fail(ResultCodeEnum.SERVER_ERROR, e);
log.error("请求的地址是:{},出现的异常是:{}", request.getRequestURL(), e);
return errorHandler;
}
/**
* 对自定义异常进行统一处理
*/
@ExceptionHandler(BusinessException.class)
public ErrorHandler handlerBusinessException(BusinessException e, HttpServletRequest request) {
ErrorHandler errorHandler = ErrorHandler.builder()
.status(e.getCode())
.message(e.getMessage())
.exception(e.getClass().getName())
.build();
log.error("请求的地址是:{},BusinessException出现异常:{}", request.getRequestURL(), e);
return errorHandler;
}
/**
* 对自定义异常进行统一处理
*/
@ExceptionHandler(OrderException.class)
public ErrorHandler handlerOrderException(OrderException e, HttpServletRequest request) {
ErrorHandler errorHandler = ErrorHandler.builder()
.status(e.getCode())
.message(e.getMessage())
.exception(e.getClass().getName())
.build();
log.error("请求的地址是:{},OrderException出现异常:{}", request.getRequestURL(), e);
return errorHandler;
}
}
@ExceptionHandler(BusinessException.class)
这个是关键语句,把自己定义的异常类注册到全局异常中,如果是运行时异常,就会自动匹配到这个类中,使用这个类中的方法进行处理
3.测试
@GetMapping("/error5")
public void error5() {
throw new BusinessException(ResultCodeEnum.LOGIN_CODE_FAIL_ERROR);
}
ResultCodeEnum.LOGIN_CODE_FAIL_ERROR
这里是在异常枚举类中自定义的异常
统一返回&异常返回进行结合处理
package com.gong.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kuangstudy.common.R;
import com.kuangstudy.exception.ErrorHandler;
import lombok.SneakyThrows;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
@ControllerAdvice(basePackages = "com.gong")
public class ResultResponseHandler implements ResponseBodyAdvice<Object> {
/**
* 是否支持advice功能,true是支持 false是不支持
*
* @param methodParameter
* @param aClass
* @return
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
//直接写return true对spingmvc中所有请求的结构不论什么数据类型都进行结果处理
// Executable executable = methodParameter.getExecutable();
// String name = executable.getName();
// Class<?> declaringClass = executable.getDeclaringClass();
// return name.equals("getCourse");
return true;
}
// 参数o 代表其实就是springmvc的请求的方法的结果
@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
// 对请求的结果在这里统一返回和处理
if (o instanceof ErrorHandler) {
// 1、如果返回的结果是一个异常的结果,就把异常返回的结构数据倒腾到R.fail里面即可
ErrorHandler errorHandler = (ErrorHandler) o;
return R.fail(errorHandler.getStatus(), errorHandler.getMessage());
} else if (o instanceof String) {
// 2、因为springmvc数据转换器对String是有特殊处理 StringHttpMessageConverter
ObjectMapper objectMapper = new ObjectMapper();
R r = R.success(o);
return objectMapper.writeValueAsString(r);
}
return R.success(o);
}
}