本文内容来自Spring Boot相关书籍学习总结
1、Web开发
Spring Boot将传统Web开发的mvc、json、validation、tomcat等框架整合,提供了spring-boot-starter-web组件,简化了Web应用配置、开发的难度。
1.1 Web入门
1. spring-boot-starter-web
Spring Boot提供的spring-boot-starter-web组件为Web应用开发提供支持,它内嵌了Tomcat服务器以及Spring MVC的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2. 实现简单的Web请求
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "hello,world";
}
}
1.2 @Controller和@RestController
Spring Boot提供了@Controller和@RestController两种注解来标识此类负责接收和处理HTTP请求。
@RestController和@Controller的区别
1)如果使用@RestController注解,则Controller中的方法无法返回Web页面,配置的视图解析器InternalResourceViewResolver不起作用,返回的内容就是Return中的数据。
2)使用@Controller注解时,在对应的方法上,视图解析器可以解析返回的JSP、HTML页面,并且跳转到相应页面。若返回JSON等内容到页面,则需要添加@ResponseBody注解。
3)@RestController注解相当于@Controller和@ResponseBody两个注解的结合,能直接将返回的数据转换成JSON数据格式,无须在方法前添加@ResponseBody注解,但是使用@RestController注解时不能返回JSP、HTML页面,因为视图解析器无法解析JSP、HTML页面。
1.3 @RequestMapping
@RequestMapping注解主要负责URL的路由映射。它可以添加在Controller类或者具体的方法上,如果添加在Controller类上,则这个Controller中的所有路由映射都将会加上此映射规则,如果添加在方法上,则只对当前方法生效。
注解的属性:
value:请求URL的路径,支持URL模板、正则表达式。
method:HTTP请求的方法。
consumes:允许的媒体类型,如consumes="application/json"为HTTP的Content-Type。
produces:相应的媒体类型,如consumes="application/json"为HTTP的Accept字段。
params:请求参数。
headers:请求头的值。
1.4 @ResponseBody
@ResponseBody注解主要用于定义数据的返回格式,作用在方法上,默认使用Jackson序列化成JSON字符串后返回给客户端,如果是字符串,则直接返回。
4、参数校验
对于应用系统而言,任何客户端传入的数据都不是绝对安全有效的,这就要求我们在服务端接收到数据时也对数据的有效性进行验证,以确保传入的数据安全正确。
4.1 Hibernate Validator简介
JSR(Java Specification Request)定义了数据验证规范,而Hibernate Validator则是基于JSR规范,实现了各种数据验证的注解以及一些附加的约束注解。Spring Validation则是对Hibernate Validator的封装整合。
引入validation组件依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
常用注解:
| 注解 | 作用目标 | 检查规则 |
|---|---|---|
| @NotNull | 属性 | 检查值是否非空 |
| @NotEmpty | 属性 | 检查字符串、集合、Map、数组非空 |
| @NotBlank | 属性 | 检查字符串非空(且不能是空格) |
| @Max | 属性 | 检查值是否小于或等于最大值 |
| @Min | 属性 | 检查值是否大于或等于最小值 |
| @Range | 属性 | 检查值是否介于最小值和最大值之间 |
| @Pattern | 属性 | 正则校验 |
4.2 数据校验
Post请求体参数检验示例:
@Data
public class TaskInfoParam {
@NotNull(message = "任务ID不能为空", groups = {Update.class, Delete.class})
private Integer taskId;
@NotBlank(message = "任务名称不能为空")
private String taskName;
private String taskDesc;
@NotBlank(message = "任务类型不能为空")
private String taskType;
@NotBlank(message = "业务类型不能为空")
private String product;
@NotBlank(message = "脚本文件路径不能为空")
private String filePath;
@NotEmpty(message = "输出节点不能为空")
private List<String> sinkIpList;
private Integer sinkBatchNum;
@NotNull(message = "消费超时时间不能为空")
@Min(value = 1L, message = "超时时间必须大于0")
private Integer batchTimeOut;
@NotNull(message = "消费进程数不能为空")
private Integer processNum;
private String taskUser;
@NotNull(message = "消费数据源ID不能为空")
private Integer sourceId;
}
在Controller方法使用@Validated进行参数校验
@PostMapping("add")
public Object addTask(@RequestBody @Validated TaskInfoParam taskInfoParam) {
//......
}
注意:
当参数校验失败后,会抛出MethodArgumentNotValidException异常,通常需要在统一异常处理中,获取错误信息并返回;
Get请求参数检验示例:
@PostMapping("uploadTemplate")
public Object uploadTemplate(@RequestParam @NotBlank(message = "ID不能为空") String editId,
@RequestParam MultipartFile file) {
//......
}
对请求参数进行参数校验时,需要在Controller类上添加@Validated注解才能生效。
注意:
如果请求体中有级联对象,且该对象属性也需要进行参数验证,此时需要在该对象属性上添加@Valid注解就可以进行验证。
4.4 分组校验
不同接口的数据校验规则可能会有差异,为每个请求单独创建请求体类并为属性设置参数校验注解非常不优雅。Hibernate Validator的注解提供了groups参数,用于指定分组,默认分组为javax.validation.groups.Default。
示例:设置增删改查四个分组接口
public interface Create extends Default {
}
public interface Update extends Default {
}
public interface Delete {
}
public interface Read{
}
其中,Create和Update都继承了Default接口,表明对未设置groups参数值的注解,也会进行属性校验。
注解请求体属性,针对需要区分校验规则的属性,在注解上添加groups值:
@NotNull(message = "任务ID不能为空", groups = {Update.class, Delete.class})
private Integer taskId;
@NotBlank(message = "任务名称不能为空")
private String taskName;
如上所示,表明只有Update和Delete分组时,才需要校验taskId非空。
使用校验分组:
@PostMapping("edit")
public Object updateTask(@Validated(Update.class) @RequestBody TaskInfoParam taskInfoParam) {
//......
}
5、拦截器
拦截器一般用于拦截用户请求,实现访问权限控制、日志记录、敏感过滤等功能。
5.1 HandlerInterceptor简介
Spring Boot定义了HandlerInterceptor接口来实现自定义拦截器的功能。HandlerInterceptor接口定义了preHandle、postHandle、afterCompletion三种方法,通过重写这三种方法实现请求前、请求后等操作。
1)preHandle:预处理回调方法实现处理程序的预处理(如登录检查),返回值:true表示继续流程(如调用下一个拦截器或处理程序);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理程序,此时需要通过response来产生响应。
2)postHandle:后处理回调方法,实现处理程序的后处理(在渲染视图之前),此时可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
3)afterCompletion:整个请求处理完之后回调方法,即在视图渲染完毕时回调,如在性能监控中,可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理(比如清空ThreadLocal),类似于try-catch-finally中的finally,但是只调用处理程序执行preHandle,返回true所对应的拦截器的afterCompletion。
5.2 使用HandlerInterceptor实现拦截器
示例:用户登录拦截校验
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if(校验未通过) {
buildReturnException(response, new CommonResult(401,"用户未登录"));
return false;
}
return true;
}
}
在省略号处,用户可以校验用户登录信息以及角色等,进行登录和鉴权拦截校验。
将拦截器注入系统配置
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public AuthInterceptor getAuthInterceptor() {
return new AuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getAuthInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/rtmp/api/common/*");
}
}
通过WebMvcConfigurer类的addInterceptors方法将刚刚自定义的LoginInterceptor拦截器注入系统中。其中,addPathPatterns定义拦截的请求地址;excludePathPatterns的作用是排除某些地址不被拦截。
当校验未通过,返回false时,建议使用response返回统一响应信息。
/**
* 统一返回结果
*/
private void buildReturnException(HttpServletResponse response, CommonResult commonResult) {
response.setCharacterEncoding("utf-8");
try (PrintWriter writer = response.getWriter()) {
writer.write(JSONUtil.toJsonStr(commonResult));
writer.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
6、过滤器
在开发Web项目时,经常需要过滤器(Filter)来处理一些请求,包括字符集转换、过滤敏感词汇等场景。
6.1 过滤器简介
过滤器是Java Servlet规范中定义的,能够在HTTP请求发送给Servlet之前对Request(请求)和Response(返回)进行检查和修改,从而起到过滤的作用。
Spring Boot内置了很多过滤器,也支持根据实际需求自定义过滤器。自定义过滤器有两种实现方式:第一种是使用@WebFilter,第二种是使用FilterRegistrationBean,一般考虑使用第二种方式。
过滤器和拦截器的区别:
过滤器和拦截器在很多应用场景上有相似之处,但其技术实现有较大差异。
1)过滤器依赖于Servlet容器,属于Servlet规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用。
2)过滤器的执行由Servlet容器回调完成,而拦截器通常通过动态代理的方式来执行。
3)过滤器的生命周期由Servlet容器管理,而拦截器可以通过IoC容器来管理,因此可以通过注入等方式来获取其他Bean的实例,使用更方便。
拦截器和过滤器的执行顺序: 先过滤器后拦截器。具体执行过程为:过滤前→拦截前→执行→拦截后→过滤后
6.2 实现过滤器
自定义过滤器的步骤:
1)添加自定义Filter类,实现Filter接口,并实现其中的doFilter()方法。
2)添加@Configuration注解,将自定义过滤器加入过滤链。
以监控请求执行时间为例,创建拦截器
public class CustomTimerFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
LocalDateTime start = LocalDateTime.now();
chain.doFilter(request, response);
LocalDateTime end = LocalDateTime.now();
Duration duration = Duration.between(start, end);
System.out.println("spend time:" + duration.toMillis());
}
}
通过FilterRegistrationBean类将定义的ConsumerTimerFilter过滤器注入系统中,并配置过滤的地址和执行顺序。
@Configuration
public class WebMvcConfig {
@Bean
public CustomTimerFilter customTimerFilter(){
return new CustomTimerFilter();
}
@Bean
public FilterRegistrationBean customFilterRegistration() {
FilterRegistrationBean<CustomTimerFilter> registrationBean =
new FilterRegistrationBean<>();
registrationBean.setFilter(customTimerFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setName("customLoginFilter");
registrationBean.setOrder(2);
return registrationBean;
}
}
7、Web配置
7.1 WebMvcConfigurer简介
WebMvcConfigurer配置类是Spring内部的一种配置方式,采用JavaBean的形式来代替传统的XML配置文件形式进行针对框架的个性化定制,可以自定义Handler、Interceptor、ViewResolver、MessageConverter。基于java-based方式的Spring MVC配置需要创建一个配置类并实现WebMvcConfigurer接口。
7.2 跨域访问
Spring Boot可以基于CORS解决跨域问题,CORS是一种机制,告诉后台哪边(Origin)来的请求可以访问服务器的数据。
/**
* 设置跨域
*/
@Bean
public CorsFilter corsFilter() {
//1.添加Cors配置信息
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("*");
//设置是否发送cookie信息
configuration.setAllowCredentials(true);
//设置允许请求的方法
configuration.addAllowedMethod("*");
//设置允许的header
configuration.addAllowedHeader("*");
//2.为url添加映射路径
UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
corsSource.registerCorsConfiguration("/**", configuration);
//3.返回重新定义好的corsSource
return new CorsFilter(corsSource);
}
7.3 数据转换配置
Spring Boot支持对请求或返回的数据类型进行转换,比如对返回的日期类型数据统一进行格式化。
以Java8的日期类型转换为例:
@Bean
public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter =
new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
JavaTimeModule module = new JavaTimeModule();
//设置序列化器
module.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
//设置反序列化器
module.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
objectMapper.registerModule(module);
converter.setObjectMapper(objectMapper);
return converter;
}
//添加转换器
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jackson2HttpMessageConverter());
}
7.4 静态资源映射
在开发Web应用的过程中,常常需要存取如图片之类的静态资源,很多时候会将它们存放在对象存储服务中。对于一些简单需求,也会考虑存放在本地磁盘上。
Spring Boot支持自定义静态资源目录,只需重写addResourceHandlers()方法即可。
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/analysis/api/img/**")
.addResourceLocations("file:/data/image/");//映射本地静态资源
}
通过addResourceHandler添加映射路径,通过addResourceLocations来指定文件查找路径,可以设置多个。
8、优雅的数据返回
8.1 为什么要统一返回值
定义统一的数据返回格式有利于提高开发效率、降低沟通成本,降低调用方的开发成本。
在项目开发中,会将响应信息封装成JSON返回,一般会统一所有接口的数据格式,使前端对数据的操作一致。响应信息一般要包含状态码、消息提示、具体数据这3部分内容,如下所示:
{
"status": 200,
"msg": "success",
"success":true,
"data": {
"list": [
{
"id": "1",
"taskName": "任务1",
"taskDesc": "描述"
}
]
}
}
8.2 统一数据返回
1. 定义数据格式
定义的返回值包含如下内容:
Integer code:成功时返回200,失败时返回具体错误码。
String message:成功时返回success,失败时返回具体错误消息。
T data:成功时返回具体值,失败时为null。
2. 定义状态码
状态码字段能够让服务端、客户端清楚知道操作的结果、业务是否处理成功,如果失败,失败的原因等信息。因此,各方需要约定好统一的状态码,一般常见的通用状态码如下:
| 状态码 | 说明 |
|---|---|
| 200 | 成功 |
| 400 | 失败 |
| 401 | 无权访问 |
| 404 | 接口不存在 |
| 500 | 服务器异常 |
定义状态码枚举类
public enum ResponseStatusEnum {
SUCCESS(200, true, "操作成功!"),
FAILED(400, false, "操作失败"),
NOT_FOUND(404,false,"资源不存在!"),
SERVER_ERROR(500, false, "未知错误!");
// 响应业务状态
private Integer status;
// 调用是否成功
private Boolean success;
// 响应消息,可以为成功或者失败的消息
private String msg;
ResponseStatusEnum(Integer status, Boolean success, String msg) {
this.status = status;
this.success = success;
this.msg = msg;
}
public Integer status() {
return status;
}
public Boolean success() {
return success;
}
public String msg() {
return msg;
}
}
3. 定义统一响应类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GraceJSONResult {
// 响应业务状态码
private Integer status;
// 响应消息
private String msg;
// 是否成功
private Boolean success;
// 响应数据,可以是Object,也可以是List或Map等
private Object data;
public GraceJSONResult(Object data) {
this.status = ResponseStatusEnum.SUCCESS.status();
this.msg = ResponseStatusEnum.SUCCESS.msg();
this.success = ResponseStatusEnum.SUCCESS.success();
this.data = data;
}
public static GraceJSONResult exception(ResponseStatusEnum responseStatus){
return new GraceJSONResult(responseStatus);
}
public GraceJSONResult(ResponseStatusEnum responseStatus) {
this.status = responseStatus.status();
this.msg = responseStatus.msg();
this.success = responseStatus.success();
}
public GraceJSONResult(ResponseStatusEnum responseStatus, String msg) {
this.status = responseStatus.status();
this.msg = msg;
this.success = responseStatus.success();
}
public GraceJSONResult(ResponseStatusEnum responseStatus, Object data) {
this.status = responseStatus.status();
this.msg = responseStatus.msg();
this.success = responseStatus.success();
this.data = data;
}
/**
* 成功返回,带有数据的,直接往OK方法丢data数据即可
*
* @param data
* @return
*/
public static GraceJSONResult ok(Object data) {
return new GraceJSONResult(data);
}
/**
* 成功返回,不带有数据的,直接调用ok方法,data无须传入(其实就是null)
*
* @return
*/
public static GraceJSONResult ok() {
return new GraceJSONResult(ResponseStatusEnum.SUCCESS);
}
/**
* 错误返回,直接调用error方法即可,当然也可以在ResponseStatusEnum中自定义错误后再返回也都可以
*
* @return
*/
public static GraceJSONResult error() {
return new GraceJSONResult(ResponseStatusEnum.FAILED);
}
/**
* 错误返回,map中包含了多条错误信息,可以用于表单验证,把错误统一的全部返回出去
* @param map
* @return
*/
public static GraceJSONResult errorMap(Map map) {
return new GraceJSONResult(ResponseStatusEnum.FAILED, map);
}
/**
* 错误返回,直接返回错误的消息
*
* @param msg
* @return
*/
public static GraceJSONResult errorMsg(String msg) {
return new GraceJSONResult(ResponseStatusEnum.FAILED, msg);
}
/**
* 错误返回,token异常,一些通用的可以在这里统一定义
* @return
*/
public static GraceJSONResult errorTicket() {
return new GraceJSONResult(ResponseStatusEnum.TICKET_INVALID);
}
}
8.3 全局异常处理
1. 全局异常处理的实现方式
使用@RestControllerAdvice、@ExceptionHandler注解实现全局异常处理,@RestControllerAdvice定义全局异常处理类,@ExceptionHandler指定自定义错误处理方法拦截的异常类型。实现全局异常捕获,并针对特定的异常进行特殊处理。
2. 全局异常处理示例
自定义异常:
public class MyCustomException extends RuntimeException {
private final ResponseStatusEnum responseStatus;
public MyCustomException(ResponseStatusEnum responseStatus) {
super("异常状态码为:" + responseStatus.status() + ";具体异常信息为:" + responseStatus.msg());
this.responseStatus = responseStatus;
}
public ResponseStatusEnum getResponseStatus() {
return responseStatus;
}
}
全局异常处理类:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理自定义异常
* @param e
* @return
*/
@ExceptionHandler(MyCustomException.class)
public GraceJSONResult returnMyException(MyCustomException e) {
log.error("catch exception: {}", ExceptionUtil.stacktraceToString(e));
return GraceJSONResult.exception(e.getResponseStatus());
}
/**
* 处理未找到异常
* @param ex
* @return
*/
@ExceptionHandler(NoHandlerFoundException.class)
public Object handleNoFoundException(NoHandlerFoundException ex) {
log.error("catch exception: {}", ExceptionUtil.stacktraceToString(ex));
return GraceJSONResult.exception(ResponseStatusEnum.NOT_FOUND);
}
/**
* 方法参数校验异常
*
* @param ex
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
Map<String, String> errors = getErrors(result);
return GraceJSONResult.errorMap(errors);
}
public Map<String, String> getErrors(BindingResult result) {
Map<String, String> map = Maps.newHashMap();
List<FieldError> list = result.getFieldErrors();
list.forEach(error -> map.put(error.getField(), error.getDefaultMessage()));
return map;
}
/**
* 处理未知异常
*
* @param ex
* @return
*/
@ExceptionHandler(Exception.class)
public Object handleException(Exception ex) {
log.error("catch exception: {}", ExceptionUtil.stacktraceToString(ex));
return GraceJSONResult.exception(ResponseStatusEnum.SERVER_ERROR);
}
}