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: wx123456789asdsad
token: ca
appSecret: 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实体类
@Autowired
private 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 {
@Resource
private 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: wx123456789asdsad
token: ca
appSecret: a123b456c789d147e258f369
port: ${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.Driver
username: root
password: wen
hikari:
connection-timeout: 30000
maximum-pool-size: 30
minimum-idle: 10
max-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;// 下面文件上传时用
@Resource
private TokenInterceptor tokenInterceptor;// 下面拦截器时用
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/images/");
// 下面文件上传时用,让匹配upload开头的URL,让他去找文件路径。
registry.addResourceHandler("/upload/**").addResourceLocations("file:" + uploadPath);
}
// 配置拦截器,下面拦截器时用
@Override
public 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
// 写一个构造方法,传递Msg
public 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方法。
@Override
public 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方法执行之后,并且视图未渲染之前进行调用。(在前后端分离开发时,没有试图这个概念,所以了解即可)
@Override
public void postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception{
HandlerInterceptor.super.postHandler(request, response, handler, modelAndView);
}
// 在整个请求处理完毕之后进行回调,所以在这个方法里可以做一些线程资源的释放
@Override
public 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) {
// 不为空,说明方法用了这个注解,就去校验token
String 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 {
// 配置快速校验模式
@Bean
public 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) {
}
以上内容源于网络。