springboot是什么
传统的开发模式下,无论是基于xml
或注解
,都要做许多配置,如果项目中集成越多的其他框架,配置内容也会越多。为了让开发人员以最少的配置去开发应用,springboot诞生了。springboot的原则是约定大于配置
(能不配就不配)。
springboot还未spring项目提供了很多非功能特性,比如嵌入式tomcat。
创建springboot项目
点击File
->New
->Project
,然后左侧选择Spring Initializr
,输入相关项目信息,点击next
,然后选择相关依赖,选择Spring Web
,点击Create
。
springboot配置文件
实际开发中,经常需要用到自定义配置
(下面是以微信公众号开发时为例)。
/src/main/resources/application.yml
wechat:appid: wx123456789asdsadtoken: caappSecret: a123b456c789d147e258f369
读取配置项的方式(一)
/src/main/java/com/asd/config/WeChat.java
@ConfigurationProperties("wechat") // 参数输入前缀。@ConfigurationProperties和@Value注解用于获取配置文件中的属性定义并绑定到Java Bean或属性中
@Component
public class WeChat {private String appId;private String token;private String appSecret;...getter setter
}
/src/main/java/com/asd/controller/HellowController.java
@RestController // 等同于@Controller + @ResponseBody
public class HellowController {// 注入WeChat实体类@Autowiredprivate WeChat weChat;...@GetMapping("/getWeChat")public WeChat getWeChat() {return weChat;}
}
重启服务后,访问 localhost:8080/getWeChat :
{"appId":"wx123456789asdsad", "token":"ca", "appSecret":"a123b456c789d147e258f369"}
读取配置项的方式(二)
/src/main/java/com/asd/config/WeChatConfig.java
@Configuration // 标识这是一个配置类
public class WeChatConfig {@Bean // 这样的话springboot就自动帮我们注入weChat对象@ConfigurationProperties("wechat")public WeChat weChat() {return new WeChat();}
}
读取配置项的方式(三)
/src/main/java/com/asd/controller/HellowController.java
@RestController
public class HellowController {@Value("${wechat.appId}")private String appId;@Value("${wechat.token}")private String token;@Value("${wechat.appSecret}")private String appSecret;...@GetMapping("/getWeChat")public WeChat getWeChat() {WeChat weChat = new WeChat();weChat.setAppId(appId);weChat.setAppSecret(appSecret);weChat.setToken(token);return weChat;}
}
如果属性较少,可以用@Value。如果属性较多,建议用第一种或第二种。
在实际项目开发中,有时需要读取自定义的配置文件。新建自定义配置文件:
/src/main/resources/my.yml
wechat1:appid: wx123456789asdsad
/src/main/java/com/asd/config/MyWeChat.java
@PropertySource("my.yml")
@ConfigurationProperties("wechat1")
@Component
public class MyWeChat {private String appId;...getter setter
}
/src/main/java/com/asd/controller/HellowController.java
@RestController
public class HellowController {@Resourceprivate MyWeChat myWeChat;...@GetMapping("/getWeChat2")public MyWeChat getWeChat2() {return myWeChat;}
}
springboot多环境配置
开发项目时,通常需要经历几个阶段。
- 本地开发接口(本地开发环境,local)
- 开发完后与前端做接口联调(前后端联调环境,dev)
- 联调完后提交测试(测试环境,test)
- 测试完后有些公司会预发布(预发布环境,pre)
- 部署到线上(生产环境,prod)
不同的开发环境,属性配置一般都不一样。如果不做多环境配置,就得去频繁修改配置文件,这样有一定的安全隐患。比如在本地开发时,不小心连上线上数据库,这样会对线上数据库造成一定的数据污染。
在resources目录下创建各环境的配置文件,springboot在启动的过程中,首先会加载application.yml,其次去加载这N个不同环境配置文件中的某一个。
在application.yml中的属性名可以当做变量,即${}来进行引用。
application-local.yml
server:port: 8001
application-dev.yml
server:port: 8002
application.yml
wechat:appid: wx123456789asdsadtoken: caappSecret: a123b456c789d147e258f369port: ${server.port}# 引用属性值,如果是引入local配置文件,其值就会是8001
spring:profiles:active: local #写你要启动的配置文件的后缀就行
获取参数
比如有实体类Student,他有name、age属性。
1)通过request对象获取参数
2)@RequestParam
(针对请求头方式为x-www-form-urlencoded,比如form表单)
@GetMapping("/get")
public Student getById(@RequestParam Integer id,@RequestParam String name) {return id;
}
如果请求参数的name是id1,就得写成@RequestParam("id1")。
3)@RequestBody
(针对请求头方式为application/json)
@PostMapping("/save")
public Student save(@RequestBody Student student) {return student;
}
4)@PathVariable
(接收url路径参数)
@GetMapping("/get/{id}")
public Student getById(@PathVariable Integer id) {return id;
}
若是{id1},与参数名id不一样,得写为@PathVariable("id1")
springboot集成mybatis
pom.xml
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.3</version>
</dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.13</version>
</dependency>
application-local.yml
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/test?driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: wenhikari:connection-timeout: 30000maximum-pool-size: 30minimum-idle: 10max-lifetime: 6000
mybatis:configuration:#配置打印sql日志log-impl: org.apache.ibatis.logging.stdout.StdOutImpl#设置xml文件扫描路径。把所有配置文件放在resources/mapper下面mapper-locations: mapper/**/*.xml
在这个例子中application.yml目前引用的是application-local.yml。下同
最后,启动类要加注解@MapperScan("com.ca.mapper")
,即扫描dao包里的接口。或在dao上加@Mapper
注解。
springboot访问静态资源
resources/static
里可存放静态资源文件,比如放一张图片后,localhost:8080/asd.jpg
就能访问。
这是springboot默认存放静态资源的目录
若想访问自定义目录下的静态资源,比如新建resources/images
目录,里面放图片。此时,需要对该文件夹做配置。
WebAppConfig.java
@Component
public class WebAppConfig implements WebMvcConfigurer {@Value("${upload.path}")private String uploadPath;// 下面文件上传时用@Resourceprivate TokenInterceptor tokenInterceptor;// 下面拦截器时用@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/static/**").addResourceLocations("classpath:/images/");// 下面文件上传时用,让匹配upload开头的URL,让他去找文件路径。registry.addResourceHandler("/upload/**").addResourceLocations("file:" + uploadPath);}// 配置拦截器,下面拦截器时用@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 默认拦截所有url// registry.addInterceptor(tokenInterceptor);// 注册拦截器// 但实际开发中,要对部分url放行。所以可以针对一些url进行匹配,匹配需要拦截的,即addPathPatterns("/**"),"/**"是所有url // 也可以配置不需要拦截的url,比如对/student/...放行。registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns("/student/*");}
}
WebMvcConfigurer配置类其实是Spring内部的一种配置方式,采用JavaBean的形式来代替传统的xml配置文件形式进行针对框架个性化定制,可以自定义一些Handler,Interceptor,ViewResolver,MessageConverter。基于java-based方式的spring mvc配置,需要创建一个配置类并实现WebMvcConfigurer 接口。
访问localhost:8080/static/test.jpg就行。
springboot上传文件
pom.xml
<!-- hutool -->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.13</version>
</dependency>
application-local.yml
upload:path: d:\\uploads\\
UploadController.java
@RestController
public class UploadController {// 文件上传的根路径@Value("${upload.path}")private String uploadPath;@PostMapping("/upload")public Result upload(MultipartFile file) throws IOException{// 获取文件名称String fileName = file.getOriginalFilename();// 获取文件后缀(通过hutool的FileUtil工具类,需要注意的是,这里没有“.”,比如只有jpg)String suffix = FileUtil.getSuffix(fileName);// 对文件名进行重命名(文件的子路径)// 可以指定文件名策略,比如:时间戳-UUID.文件后缀String url = shijianchuo + uuid + "." + suffix;// 文件上传的真实路径String filePath = uploadPath + url;// 获取文件的字节流InputStream inputStream = file.getInputStream();// 把这个字节流写入到保存路径FileUtil.writeFromStream(inputStream, filePath);// 把文件的子路径返回到前端return Result.success(url);}
}
访问刚才上传的文件:localhost:8080/upload/上传的文件路径.jpg
统一异常处理
调用接口过程中,可能会发生各种异常。此时,springboot默认给前端响应500
,但即便发生异常,也应该给前端做一个正常的响应,以及告诉前端响应错误的原因。
有人会说用try catch
,但如果项目越来越大,每个方法都用try catch
,代码就会越来越臃肿。springboot中可以对项目的异常做一个统一的拦截处理。
/exception/BusinessException.java
public class BusinessException extends RuntimeException { // 这是自定义异常类,要继承RuntimeException// 写一个构造方法,传递Msgpublic BusinessException(String msg) {super(msg);}
}
/exception/SystemExceptionHandler.java
// 对系统异常做一个统一的处理(全局异常处理器)。
@RestControllerAdvice // 等同于 @ControllerAdvice + @ResponseBody。此注解通过对异常的拦截实现了统一异常返回处理
public class SystemExceptionHandler {@ExceptionHandler(BusinessException.class) // 指定要拦截的异常类public Result handlerException(BusinessException e) {// 也可以当做参数使用// 当我们拦截到异常后,直接给前端返回return Result.fail(e.getMessage());}// 也可以拦截Exception@ExceptionHandler(Exception.class)public Result exception() {return Result.fail("系统异常");}// 在参数校验时使用// 在这里拦截MethodArgumentNotValidException异常@ExceptionHandler(MethodArgumentNotValidException.class)public Result methodArgumentNotValidException(MethodArgumentNotValidException e) {// 拿到所有的参数校验失败的提示List<String> errorMessage = e.getBindingResult().getAllErrors() //获取所有的错误提示,返回的是集合对象.stream() // 对集合对象进行遍历.map(DefaultMessageSourceResolvable::getDefaultMessage) // 获取message.collect(Collectors.toList()) // 然后返回集合对象// 可以使用hutool的工具包,判断集合是否为空if (CollUtil.isNotEmpty(errorMessage)) {// 如果不为空,直接取他的第一个元素,得到错误提示String errorMsg = errorMessage.get(0);// 把错误提示信息返回到前端return Result.fail(errorMsg);}return Result.fail("系统异常");}}
配置后,出现异常时,就会返回Result这个统一的restful返回信息了。
如果想拦截自定义的BusinessException类,在代码中写throw new BusinessException("错误");即可。
拦截器
拦截器的拦截对象是controller里的方法。
例子:判断客户端发送的请求头中,是否包含token,并且是否值为asd。
/interceptor/TokenInterceptor.java
@Component
public class TokenInterceptor implements HandlerInterceptor{// 当返回true时执行controller里面的代码,但返回false时不执行下面的postHandler afterCompletion方法。@Overridepublic boolean preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) {// 所以在这里面可以对token进行拦截String token = request.getHeader("token");// 获取请求头里面的Token if (StrUtil.isBlank(token)) {throw new BusinessException("请求头未包含token");}if (!"asd".equals(token)) {throw new BusinessException("请求头参数错误");}return true;}// controller方法执行之后,并且视图未渲染之前进行调用。(在前后端分离开发时,没有试图这个概念,所以了解即可)@Overridepublic void postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception{HandlerInterceptor.super.postHandler(request, response, handler, modelAndView);}// 在整个请求处理完毕之后进行回调,所以在这个方法里可以做一些线程资源的释放@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {HandlerInterceptor.super.afterCompletion(request, response, handler, ex)}
}
执行顺序是preHandler、controller的方法、postHandler、aferCompletion。
用注解方式
/anotation/NeadToken.java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)// 设置此注解只能用于方法
public @interface NeedToken {}
@Retention
作用是定义被它所注解的注解保留多久,一共有三种策略,定义在RetentionPolicy枚举中.
source
:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;被编译器忽略
class
:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期
runtime
:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。
修改preHandler
方法(当controller的方法加上这个注解时,我们才去判断他的请求头中是否有token。):
@Override
public boolean preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) {// 判断handler是不是HandlerMethod if (handler instanceof HandlerMethod) {// 转成HandlerMethod对象HandlerMethod handlerMethod = (HandlerMethod) handler;// 通过handlerMethod可以去获得方法上面的注解NeedToken needToken = handlerMethod.getMethod().getAnnotation(NeedToken.class);if (needToken != null) {// 不为空,说明方法用了这个注解,就去校验tokenString token = request.getHeader("token");// 获取请求头里面的Token if (StrUtil.isBlank(token)) {throw new BusinessException("请求头未包含token");}if (!"asd".equals(token)) {throw new BusinessException("请求头参数错误");}}}return true;
}
测试时,为了方便,应该去把url匹配部分注释。
registry.addInterceptor(tokenInterceptor)//.addPathPatterns("/**")//.excludePathPatterns("/student/*");
springboot aop
aop五大通知:
- 前置通知 before advice:在目标方法执行之前执行
- 后置通知 after returning advice:在目标方法执行之后执行
- 异常通知 after throwing advice:目标方法抛出异常后执行
- 最终通知 after finally advice:在目标方法执行之后都会执行(发生异常时,不执行后置通知,但执行最终通知,这是这两的区别)
- 环绕通知 around advice:可以在目标方法执行之前执行,也可以在目标方法执行之后执行。
pom.xml
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
/aop/LogAspect.java
@Aspect // 标识这是一个切面类
@Component
public class LogAspect {// @Pointcut用来标识切点// RestfulStudentController.*(),指RestfulStudentController里面的所有方法@Pointcut("execution(* com.ca.controller.RestfulStudentController.*())") public void pointcut() {} // 前置通知@Before("pointcut()") // "pointcut":加切点,加上pointcut这个方法public void before() {logger.info("执行before方法");}// 最终通知@After("pointcut()")public void after() {logger.info("执行after方法");}// 后置通知@AfterReturning("pointcut()")public void afterReturn() {logger.info("执行afterReturn方法");}// 异常通知@AfterThrowing("pointcut()")public void ex() {logger.info("执行ex方法");}
}
执行后:
没异常的话,顺序为before、控制器的接口方法、afterReturn、after。
有异常的话,顺序为before、控制器的接口方法、ex、after。
补充
@Pointcut("execution(* com.ca.controller.RestfulStudentController.*(..))") // 所有方法的所有参数.*(..)
public void pointcut() {}@Before("pointcut()")
public void before(JoinPoint joinPoint) {// 这里可以加参数 JoinPoint joinPoint(@After、@AfterReturning亦是)// 通过joinPoint可以获取目标方法的参数Object[] args = joinPoint.getArgs();// 还可以通过joinPoint获取目标方法MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();Method method = methodSignature.getMethod();// 获取目标方法logger.info("拦截目标方法:{}", method.getName());logger.info("目标方法参数:{}",JSONUtil.toJsonStr(args));// 转换为json后输出。这时候要取pointcut里修改为拦截有参数的方法。logger.info("执行before方法");
}@AfterThrowing(value="pointcut()", throwing="exception")// 然后注解里要写上throwing="exception"
public void ex(Exception exception) {// 这里也可以传递异常对象,就能获取到是什么异常对象了// 之后可以打印异常logger.error(exception.getMessage(), exception);logger.info("执行ex方法");
}
环绕通知
顾名思义,就是对目标方法进行包围。使用环绕通知时,就不用使用其他通知了,可以统统注释掉。
@Around("pointcut()")
public void around(ProceedingJoinPoint proceedingJoinPoint) {// 里面可以传递 ProceedingJoinPoint proceedingJoinPoint 参数// 执行目标方法前执行前置通知System.out.println("执行前置通知");Object result;try {// 通过proceedingJoinPoint也可以参数和目标方法Object[] args = proceedingJoinPoint.getArgs();MethodSignature methodSignature = (MethodSignature)proceedingJoinPoint.getSignature();Method method = methodSignature.getMethod();logger.info("拦截目标方法:{}", method.getName());logger.info("目标方法参数:{}",JSONUtil.toJsonStr(args)); result = proceedingJoinPoint.proceed();// 执行目标方法}catch(Throwable e) {// 还可以捕获异常logger.error(e.getMessage(), e);}// 执行目标方法后执行后置通知System.out.println("执行后置通知");return result;
}
springboot事务
事务指的是多个操作(插入、更新、删除等)同时进行,要么同时成功或同时失败。
需要用到事务的地方,在方法上添加@Transactional
。
注意事项
1)比如service里有save和save1方法,本来save上有@Transactional。后来把save里面的代码和@Transactional都干到save1上,然后在save里调用save1。这时,若出现异常也不会回滚。因为@Transactional使用了aop,aop首先要拦截的方法是save,而不是save1,所以他并没有获取到事务注解,此时事务就直接交给数据库自动控制了。所以@Transactional得加到save上才行(然后干掉save1上@Transactional)。
2)假如@Transactional加到save,在save中使用try catch,此时就算有异常,也不会回滚。因为aop只看这个方法是否抛出异常,抛出了才回滚。所以在catch里throw ex;即可。
3)目前service里有save,加上了@Transactional。假如有service1,里面有save1。在service的save里调用service1.save1(),而service1的save1加了@Transactional(propagation = Propagation.REQUIRES_NEW),REQUIRES_NEW是表示每次都重新开启一个新的事务。此时,在save中做了三个操作(加管理员,加管理员角色,[刚刚追加的]加角色)能否同时成功或失败?执行后可以发现,save里有异常时前两个失败,但save1成功了,这是因为save1开启了新的事务,所以他自己就提交了。所以,要保证所有的操作都要在同一个事务里面。
4)创建自定义异常类TestException(继承了Exception)。然后在save里throw new TestException();,但发现不回滚。因为@Transactional默认拦截的异常需要继承RuntimeException。但可以通过@Transactional(rollbackFor = Exception.class)来解决。
参数校验
在controller里用传统的if方式校验参数时,若参数多,就会代码冗余。为了解决这个,有必要使用参数校验框架springboot validation。
他允许我们通过注解的方式来定义对象校验规则,把校验和业务逻辑分离。
springboot validation 常用注解如下:
- @NotBlank(校验字符串不为null,并且不为空字符串)
- @NotEmpty(校验字符串不为null,允许空字符串)
- @NotNull(校验对象不为null)
- @Length(校验字符串长度)
- @Min(最小值校验)
- @Max(最大值校验)
- @Pattern(正则匹配校验),比如邮箱号、手机号时可以使用这个
- @Valid(当一个对象嵌套另外一个对象时可使用)
class Student {
@Valid
private Admin admin;
}
当我们使用学员类去接收参数时,我们也需要去校验Admin里面的参数的时候,就可以使用@Valid去进行校验。
pom.xml
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Student.java
public class Student {@NotBlank(message="姓名不能为空")@Length(max = 5, message = "姓名不能超过5个字符")private String name;@NotNull@Max(value=100, message="年龄不能大于100岁")@Min(value=1, message="年龄不能小于1岁")private Integer age;@NotNull@Min(value = 1)@Max(value = 2)private Integer sex;
}
StudentController.java
@PostMapping
public Student save(@RequestBody @Valid Student student) {}
也可以使用@Validated
,这个是spring里面
的注解,@Valid
是javax
里面的注解,这两个是都可以的,但有点区别。
运行后,若参数有问题,就会校验不通过,抛出MethodArgumentNotValidException
异常,但不告诉前端是什么问题。
当然,可以用全局异常拦截器来解决这个问题。
校验模式
springboot validation分两种校验模式,全校验
、快速校验
(推荐)。
全校验
指的是对所有的参数都校验完毕之后进行返回,快速校验
是指只要遇到接口参数校验失败就立即返回。
使用快速校验模式
时,需要做一个单独的配置,只需要把下面代码复制到项目即可。
BeanConfig.java
@Configuration
public class BeanConfig {// 配置快速校验模式@Beanpublic Validator validator() {ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure().failFast(true)// 配置校验失败就立即停止.buildValidatorFactory();Validator validator = validatorFactory.getValidator();return validator;}
}
其他
使用springboot validator时,可能会遇到一个问题,比如save和update时,若都用Student来接收参数的话,save时虽然不需要做id校验,但update时则需要做id校验。此时,可对校验做分组。
比如@NotNull(groups = Update.class)
,groups =
这里要设置一个类(得是接口),所以先创建两个接口。若设置为groups = Update.class
就说明要在修改时才做校验。
/valid/Save.java
public interface Save {}
/valid/Update.java
public interface Update {}
Student.java
public class Student {@NotNull(groups=Update.class)private Integer id;@NotBlank(message="姓名不能为空", groups={Save.class, Update.class})// 也可以指定多个分组@Length(max = 5, message = "姓名不能超过5个字符", groups=Save.class)private String name;@NotNull@Max(value=100, message="年龄不能大于100岁", groups=Save.class)@Min(value=1, message="年龄不能小于1岁", groups=Save.class)private Integer age;@NotNull@Min(value = 1, groups=Save.class)@Max(value = 2, groups=Save.class)private Integer sex;
}
然后还要把controller里的@Valid
改为springboot里的@Validated(Save.class)
,即也指定了分组。
StudentController.java
@PostMapping
public Student save(@RequestBody @Validated(Save.class) Student student) {}@PutMapping
public Student update(@RequestBody @Validated(Update.class) Student student) {}
以上内容源于网络。