spring-boot 笔记
spring boot 就是一个内嵌web容器的可执行程序的框架。
1. idea修改背景颜色
旧版本的Intellij idea(Files-settings-editor-color&font然后选择黑色或者白色)
新版本的Intellij idea(Files-settings-editor-color scheme,然后右侧框中选择(Default(白色)或者Darcula(黑色)))
2. 新建spring boot 项目(小工程)
-
file -> new -> project 选择spring initializr 和default 点击next
-
修改Java version和version ,改为8和1.0.0版本 ,点击next
-
选择Web,然后选Spring Web,点击next
-
接着点击next,直到finish
3. 启动项目
-
新建一个包,叫做controller
-
在controller中建一个接口类,名字叫做HelloController.class,写出下列代码:
@RestController public class HelloController { @GetMapping("/home") public String homme(){ return "hello world!"; } }
-
在网站中输入网址:http://127.0.0.1:8080/home 就完成了。
4. 新建 spring boot项目(大工程)
-
1,2步和小工程一样,第三步是什么都不选,直接点next。
-
项目建成之后,删去.mvn、src、.gitignore、HELP.md、mvnw、mvnw.cmd文件,将小工程的文件夹移动到大工程文件夹的目录下。
-
将大工程文件夹下的 pom 文件的 dependencies 内的内容删掉。
-
将小工程再复制粘贴到该路径,改为 config ,删去 src 和 pom 之外的所有文件。
-
将 config 工程里的 pom 文件的 name 和 description 删去,将 artifactId 的内容改为 config 。
-
在大工程的pom文档中添加模块内容
<modules> <module>config</module> </modules>
5. 常用配置
-
改端口:找到目标工程 resources 目录下的 application.properties ,添加代码
server.port = 9090
-
改随机端口:如果在一台服务器上,多个服务器使用固定端口会造成端口冲突,并且在现实的微服务开发中,开发人员是不用记住ip和端口的,因此,我们在一般的真实开发环境中,会设置一个随机端口,就不用去管理端口了,也不会造成端口冲突。
server.port=${random.int[1024,9999]} // 是指随机1024-9999之间的端口号
6. 自定义配置
@Value 的作用:为了简化读取properties文件中的配置值,spring支持@value注解的方式来获取,这种方式大大简化了项目配置,提高业务中的灵活性。
在properties中添加 ping.msg=hi,hello world!!!
在接口类中添加代码:
@RestController
public class HelloController {
@Value("${ping.msg}")
private String msg;
@GetMapping("msg")
public String getMsg(){
return msg;
}
@GetMapping("/home")
public String homme(){
return "hello world!";
}
}
运行后,输入 http://127.0.0.1:9090/msg 可以打印msg的信息。
7. yml文件配置
-
spring boot的配置文件有两种,一种是一properties结尾,一种是以yaml或yml结尾。
-
yml是以空格的缩进程度来控制层级关系。(空格个数不重要)
-
对比区别:(代码一定要变颜色)
//以properties结尾 server.port=9090 ping.msg=hi,hello world!!! //以yml结尾 server: port: 9090 ping: msg: hi,hello world!!!
8. 日志配置
比较流行的日志框架:logback、log4j,从spring boot 的底层框架***spring-boot-starter-logging***可以看出,它依赖了三个框架:slf4j、logback、log4j。
-
logback、log4j:日志实现框架,实现怎么记录日志。
-
slf4j:提供了Java中所有的日志框架的简单抽象(日志的门面设计模式),就是一个日志API,没有实现类,不能单独使用。必须结合logback或log4j来实现。
-
打印日志的代码:(在接口类中实现)
private static final Logger logger = LoggerFactory.getLogger(HelloController.class); @GetMapping("/log") public void log(){ logger.trace("----------------------trace--------"); logger.debug("----------------------debug--------"); logger.info("----------------------info--------"); logger.warn("----------------------warn--------"); logger.error("----------------------error--------"); }
打印之后,只会显示info、warn、error,因为spring boot默认是info级别的。或者在配置文件中添加下列语句就可以了。
logging.level.com.example.boot=trace
-
将项目的日志另存在一个文件中,需要在配置文件中添加:
logging.file.path=output/logs;
会使文件中出现一个output文件夹,里面存放的是日志。 自定义路径 需要在在配置文件中添加:
logging.file.name=/java/springboot.log //保存在大工程所在的Java文件夹内,创造一个日志文件
-
修改日志格式: %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
9. lombok的使用
lombok是一个插件,也是一个依赖jar包。可以提升开发效率
1. 安装lombok插件
idea搜索lombok插件;file -> Setting ->Plugins -> 搜索 -> Browse repositories或直接找到,安装完成之后,重启idea。
2. @Data注解的使用
@Data 注解在实体类上,自动生成javabean的getter/setter方法,写构造器、equals等方法。
使用时需要在pom文件中添加依赖包
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
在实体类前使用@Data注解可以省略getter/setter方法。例如:
import lombok.Data;
@Data
public class UserVo {
private Integer id;
private String username;
private String password;
private Byte sex;
private Byte deleted;
private Data updateTime;
private Data creatTime;
}
在该实体类中不用写getId()、setId()等方法就可以在其他类中调用。
3. @Slf4j注解的使用
@Slf4j的作用是代替代码,如下
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
使得以后不用每个函数都写一遍了。
4. 代码示例
@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("guanping");
return userVo;
}
@RequestMapping("/log")
public void log(){
log.trace("------------trace---------");
log.debug("------------debug---------");
log.info("------------info---------");
log.warn("------------warn---------");
log.error("------------error---------");
}
}
10. 异步框架
使用异步处理的原因(用注册用户送积分举例):容错性,送积分异常,不能因为这个原因导致注册用户失败;提升性能,注册需要20毫秒,送积分50毫秒,同步是70毫秒,异步只用20毫秒。(注册是主要功能,送积分是次要功能)
只需要 使用@Async注解 就可 实现方法的异步调用。
1. @Async异步调用步骤
- 开启异步任务
采用@EnableAsync来开启异步任务支持,另外需要加入@Configuration来把当前类加入SpringIOC容器中。
@Configuration
@EnableAsync
public class SyncConfiguration{
}
-
在方法上标明异步调用
@Service @Slf4j public class ScoreService { //TODO 模拟睡5秒,用于赠送积分处理(次要任务) @Async public void addScore(){ try { Thread.sleep(1000*5); log.info("--------处理积分--------"); }catch (InterruptedException e){ e.printStackTrace(); } } } @RestController @Slf4j public class TestController { //TODO 用于注册新用户(主要任务) @Autowired private ScoreService scoreService; @RequestMapping("/sync") public String creatUser(){ log.info("-----------注册新用户---------"); scoreService.addScore(); return "OK"; } }
@Slf4j添加在方法上,代表该方法为异步处理。
-
@Async注解,在 默认情况下用的SimpleAsyncTaskExecutor线程池,该线程池不是真正意义上的线程池,因为线程不重用,每次调用都会新建一条线程。我们可以通过控制台日志输出查看,每次打印的线程名称都是[task-1]、[task-2]。
-
@Async注解异步框架提供多种线程
SimpleAsyncTaskExecutor:不是真的线程池。不重用线程,每次调用都会创建新的线程。
SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才考虑它。
ThreadPoolTaskScheduler:可以使用cron表达式。
ThreadPoolTaskExecutor:最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExeutor的包装。
-
为@Async实现一个自定义线程池
-
配置线程池
@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.CallerRunsPolicy():重试添加当前任务,自动重复调用execute()方法,直到成功。 * ThreadPoolExecutor.AbortPolicy():丢弃任务并抛出RejectedExecutionException异常 * ThreadPoolExecutor.DiscardOldestPolicy():丢弃队列最前面的任务,然后尝试执行任务(重复此过程) * ThreadPoolExecutor.DiscardPolicy():也是丢弃任务,但是不抛出异常 */ taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //初始化线程 taskExecutor.initialize(); return taskExecutor; } }
-
为@Async指定线程池名字
@RequestMapping("/sync2") public String creatUser2(){ log.info("-----------注册新用户2---------"); scoreService.addScore2(); return "OK"; } @Async("scorePoolTaskExecutor") public void addScore2(){ try { Thread.sleep(1000*5); log.info("--------处理积分2--------"); }catch (InterruptedException e){ e.printStackTrace(); } }
-
11. swagger生成接口文档
swagger:采用自动化实现并解决了人力编写接口文档的问题,他通过在接口及实体上添加几个注解的方式就能在项目启动后自动生成接口文档。
swagger的优点:
- 自动生成文档:只需要少量的注解,Swagger就可以根据代码自动生成API文档,很好的保证了文档的时效性。
- 跨语言性:支持40多种语言
- Swagger UI呈现出来的是一份可交互式的API文档,我们可以直接在文档页面尝试API的调用,省去了准备复杂的调用参数的过程。
- 换可以将文档规范导入相关工具,这些工具将会为我们自动的创建自动化测试。
步骤:
-
在pom文件中加入依赖包
<!--swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <!--swagger-ui--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>
-
修改配置文件
application.properties 加入配置
Swagger一般线上环境是关闭的(
spring.swagger2.enabled=true
)增加一个swagger配置类
@Configuration @EnableSwagger2 public class SwaggerConfig { @Value(value = "true") private Boolean swaggerEnabled; @Bean public Docket createRestApi(){ return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .enable(swaggerEnabled) .select() .apis(RequestHandlerSelectors.basePackage("com.example.boot"))//填写根目录 .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo(){ return new ApiInfoBuilder() .title("接口文档") .description("Spring boot API") .termsOfServiceUrl("https://study.163.com/provider/1016671292/index.htm") .version("1.0") .build(); } }
运行后,在网址输入网址:http://127.0.0.1:9090/swagger-ui.html
注意点:createRestApi()这个方法一定要写上包名,代表需要生成接口文档的目录包
-
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 Data updateTime; @ApiModelProperty(value = "创建时间") private Data creatTime; } @Api(description = "用户接口") @RestController @Slf4j @RequestMapping("/user") public class UserController { // private static final Logger logger = LoggerFactory.getLogger(UserController.class); @ApiOperation("修改某条数据") @GetMapping(value = "/u/{id}") public UserVo findById(@PathVariable int id){ Random rand = new Random(); UserVo user = new UserVo(); user.setId(id); String temp = "temp01"; user.setUsername(temp); user.setPassword(temp); int n = rand.nextInt(); user.setSex((byte)n); return user; } @ApiOperation("单个用户查询,按userid查用户信息") @PostMapping(value = "/user/create",produces = APPLICATION_JSON_UTF8_VALUE,consumes = APPLICATION_JSON_UTF8_VALUE) public UserVo crreteUser(@RequestBody UserVo userVo){ return userVo; } }
12. 接口返回值统一标准格式
-
springboot默认情况下的response的格式
-
response为String
@GetMapping(value = "/getStr") public String getStr(){ return "String"; }
-
response为object
@GetMapping(value = "/getObject") public UserVo getObject(){ UserVo vo = new UserVo(); vo.setUsername("GuanPing"); return vo; }
-
response为void
@GetMapping(value = "/empty") public void empty(){ }
-
response为异常
@GetMapping(value = "/error") public void error(){ int i = 9/0; }
-
接口如果没有一个统一的格式,客户端的开发人员会不知道如何处理返回值,因此我们需要统一response的标准格式。
-
-
response的标准格式包含三部分:
status状态值:代表本次请求response的状态结果
response描述:对本次状态码的描述
data数据:本次返回的数据
{ "status":0, "desc":"成功", "data":"test" }
-
初级程序员对response的代码封装
-
把标准格式转化为代码
@AllArgsConstructor @NoArgsConstructor @Data public class Result<T> { private Integer status; private String desc; private T data; //成功,指定ResultCode枚举 public static Result suc(){ Result result = new Result(); result.setResultCode(ResultCode.SUCCESS); return result; } //成功,创建Result,有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枚举转换为Result private void setResultCode(ResultCode code){ this.status = code.code(); this.desc = code.message(); } }
-
把状态码存在枚举类里
public enum ResultCode { SUCCESS(0,"成功"), SYSTEM_ERROR(10000,"系统异常,请稍后重试"), UNAUTHORIZED(10401,"签名验证失败"), PARAM_IS_INVALID(10001,"参数无效"), 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; } }
-
在 总的pom文件 中添加配置
<dependency> <groupId>com.example.boot</groupId> //包名 <artifactId>config</artifactId> //模块名 <version>1.0.0-SNAPSHOT</version> </dependency>
-
添加一个体验类
@Api(description = "用户接口") @RestController @Slf4j @RequestMapping("/user") public class UserController { @GetMapping(value = "/getResult") public Result getResult(){ return Result.suc("test"); }
运行后,返回的结果为
{ "status": 0, "desc": "成功", "data": "test" }
这样封装代码会有很大弊端,因为以后每写一个接口,都要手工指定Result.suc() 这行代码。
-
-
高级程序员对response的代码封装
前三步和初级相同,区别在最后一步
ResponseBodyAdvice的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来做response的统一格式,加解密、签名等。
-
ResponseBodyAdvice接口源码
public interface ResponseBodyAdvice<T> { //是否支持advice功能,true=支持,false=不支持 boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2); //处理response的具体业务的方法 @Nullable T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3,Class<? extends HttpMessageConverter<?>> var4); }
-
写一个ResponseBodyAdvice实现类
@ControllerAdvice(basePackages = "com.example.boot") public class ResponseHandler implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2) { return true; } @Override public Object beforeBodyWrite(Object var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4) { if(var1 instanceof ErrorResult){ ErrorResult errorResult = (ErrorResult)var1; return Result.fail(errorResult.getStatus(),errorResult.getMessage()); }else if(var1 instanceof String){ return JsonUtil.object2Json(Result.suc(var1)); } return Result.suc(var1); } }
@ControllerAdvice注解可以增强Controller的扩展功能:
对Controller全局数据统一处理和对Controller全局异常统一处理
使用注解时,要注意加上basePackages,如果不加上的话,他会对整个系统的Controller做了扩展功能,也会对某些特殊功能产生冲突,例如,使用Swagger会出现空白页异常。
beforeBodyWrite方法的response类型判断
if(var1 instanceof String){ return JsonUtil.object2Json(Result.suc(var1)); }
一定要加以上代码,因为Controller的返回值为String时,他直接返回String,不是json,因此我们要手动做json的转换处理。
13. 全局异常处理
全局异常处理器就是把整个系统的异常统一进行处理。
使用全局异常的原因:
-
不用强制写try-catch,由全局异常处理器统一捕获处理
-
自定义异常,只能用全局异常来捕获
@PostMapping(value = "/error4") public void error4(){ throw new RuntimeException("用户已存在!!"); }
不可能直接返回客户端,因为客户端那边看不懂,而且需要集成进《接口返回值统一标准格式》
-
JSR303规范的Validator参数校验器,参数校验不通过会抛异常,是无法使用try-catch语句直接捕获的,只能使用全局异常处理器。
1. 编码实现一个spring boot的全局异常处理器
-
封装异常内容,统一存储在枚举类中
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; } }
所有的未知运行的异常都用
SYSTEM_ERROR(10000,"系统异常,请稍后重试")
,来提示 -
封装Controller的异常结果
@Builder @AllArgsConstructor @NoArgsConstructor @Data public class ErrorResult { /* 异常状态码*/ private Integer status; /* 用户看得见的异常,例如 用户名重复 */ private String message; /* 异常名字 */ private String exception; /* 对异常提示语进行封装 */ 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()); return result; } }
-
加个全局异常处理器,对异常进行处理
@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对象。
@RestControllerAdvice增强Controller的扩展功能,全局异常处理器,就是扩展功能之一。
@ExceptionHandler 统一处理某一类异常,从而减少代码重复率和复杂度。
@ResponseStatus指定客户端收到http状态码,这里配置500错误,客户端就显示500错误。
-
体验效果
@PostMapping(value = "/error1") public void error1(){ int i=9/0; } 结果: { "status": 10000, "message": "系统异常,请稍后重试", "exception": "java.lang.ArithmeticException" }
2. 把自定义异常集成在 全局异常处理器
-
封装一个自定义异常
@Data public class BusinessException extends RuntimeException{ protected Integer code; protected String message; public BusinessException(ResultCode resultCode){ this.code = resultCode.code(); this.message = resultCode.message(); } }
自定义异常通常是集成RuntimeException
-
把自定义异常集成进全局异常处理器
/*处理自定义异常*/ @ExceptionHandler(BusinessException.class) public ErrorResult handleBusinessException(BusinessException e,HttpServletRequest request){ ErrorResult errorResult = ErrorResult.builder().status(e.code) .message(e.message) .exception(e.getClass().getName()) .build(); log.warn("URL:{},业务异常",request.getRequestURI(),errorResult); return errorResult; }
全局异常处理器找要在上节课的基础上添加一个自定义异常处理
-
体验异常
@PostMapping(value = "/error3") public void error3(){ throw new BusinessException((ResultCode.USER_HAS_EXISTED)); } { "status": 20001, "message": "用户名已存在", "exception": "com.example.video.exceptions.BusinessException" }
3. 把全局异常处理器集成进接口返回值统一标准格式
-
把全局异常处理器的json格式转换为接口返回值统一标准格式
{ "status": 20001, "message": "用户名已存在", "exception": "com.example.video.exceptions.BusinessException" } 转化为: { "status":20001, "desc":"用户名已存在", "data":null }
-
改造ResponseHandler.class
@ControllerAdvice(basePackages = "com.example.boot") public class ResponseHandler implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2) { return true; } @Override public Object beforeBodyWrite(Object var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4) { if(var1 instanceof ErrorResult){ ErrorResult errorResult = (ErrorResult)var1; return Result.fail(errorResult.getStatus(),errorResult.getMessage()); }else if(var1 instanceof String){ return JsonUtil.object2Json(Result.suc(var1)); } return Result.suc(var1); } }
在原来的基础上,添加
if(var1 instanceof ErrorResult){ ErrorResult errorResult = (ErrorResult)var1; return Result.fail(errorResult.getStatus(),errorResult.getMessage()); }
-
体验效果
14. 参数校验器(Validator)
…待更新