首页> 博客> SpringBoot 2.x 进阶 之 Web
28 01 2019
一、内容说明

接着上一篇,SpringBoot2.x 教你快速入门,本篇内容我们来学习 SpringBoot2.X 进阶 Web 方面开发常用的一些知识点。

1.1、简介

步骤:

  1. 创建SpringBoot应用,选择相应的Starter
  2. 在配置文件中指定必要的少量配置
  3. 编写业务代码

Web开发的自动配置类:WebMvcAutoConfiguration

二、静态资源的映射
2.1、静态资源的位置

查看WebMvcAutoConfiguration——>getStaticLocations()

静态资源的默认位置:

  • "classpath:/META-INF/resources/"
  • "classpath:/resources/"
  • "classpath:/static/"
  • "classpath:/public/"

当然我们可以再配置文件中修改静态资源的路径:

# 指定静态资源的位置
spring.resources.static-locations=classpath:/static,classpath:/public

备注:static 下的静态资源图片以及静态html文件,通过浏览器可以直接访问到

2.2、欢迎页

favicon.ico 放到任意一个静态资源文件夹中即可!

三、表单验证
3.1、简介

前台提交一些表单时候,往往有一些字段内容需要我们校验一下,比如:姓名、密码、年龄、字段非空,字段长度限制,邮箱格式验证呀等等这些类型。当然前端也可以做一些校验,但后端如果也要做一些信息校验时候,我们如何来做呢?

3.2、校验相关的注解
@Null 只能是null
@NotNull 不能为null 注意用在基本类型上无效,基本类型有默认初始值
@AssertFalse 必须为false
@AssertTrue 必须是true

字符串/数组/集合检查:(字符串本身就是个数组)
@Pattern(regexp="reg") 验证字符串满足正则
@Size(max, min) 验证字符串、数组、集合长度范围
@NotEmpty 验证字符串不为空或者null
@NotBlank 验证字符串不为null或者trim()后不为空

数值检查:同时能验证一个字符串是否是满足限制的数字的字符串
@Max 规定值得上限int
@Min 规定值得下限
@DecimalMax("10.8") 以传入字符串构建一个BigDecimal,规定值要小于这个值 
@DecimalMin 可以用来限制浮点数大小
@Digits(int1, int2) 限制一个小数,整数精度小于int1;小数部分精度小于int2
@Digits 无参数,验证字符串是否合法
@Range(min=long1,max=long2) 检查数字是否在范围之间
这些都包括边界值

日期检查:Date/Calendar
@Post 限定一个日期,日期必须是过去的日期
@Future 限定一个日期,日期必须是未来的日期

其他验证:
@Vaild 递归验证,用于对象、数组和集合,会对对象的元素、数组的元素进行一一校验
@Email 用于验证一个字符串是否是一个合法的右键地址,空字符串或null算验证通过
@URL(protocol=,host=,port=,regexp=,flags=) 用于校验一个字符串是否是合法URL
3.3、表单验证方法

这里简单举例来说明下,如何使用注解的方式来进行表单校验。

在实体 Bean 里需要校验的字段上面添加注解

package com.xmlvhy.girl.entity;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

/**
 * Author: 小莫
 * Date: 2019-01-18 17:43
 * Description:<描述>
 */
@Entity
@Data
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;

    //@Min(value = 18, message = "未成年禁止入内")
    private Integer age;

    @NotNull(message = "金钱不能为空")
    private Integer money;
}

Controller 中接收参数时,使用 @Valid 注解进行校验

@PostMapping(value = "girls")
public Result <Girl> girlAdd(@Valid Girl girl, BindingResult result){
	if (result.hasErrors()) {
            return ResultUtil.fail(1,result.getFieldError().getDefaultMessage());
        }
        return ResultUtil.success(girlRepository.save(girl));
}

@Valid 和 BindingResult 是相对应的,如果有多个 @Valid,那么每个 @Valid 后面跟着的 BindingResult 就是这个 @Valid 的验证结果,顺序不能乱。

由于我们示例演示,返回都是json数据,这里定义了返回结果的类

定义返回结果的最外层实体类封装

package com.xmlvhy.girl.entity;

import lombok.Data;

/**
 * Author: 小莫
 * Date: 2019-01-25 15:43
 * Description:<描述>
 */
@Data
public class Result<T> {
    /*错误码*/
    private Integer code;
    /*提示信息*/
    private String message;
    /*具体的内容*/
    private T data;
}

封装一个工具类

package com.xmlvhy.girl.util;

import com.xmlvhy.girl.entity.Result;

/**
 * Author: 小莫
 * Date: 2019-01-25 15:54
 * Description:<描述>
 */

public class ResultUtil {

    public static Result success(Object data){
        Result result = new Result();
        result.setCode(0);
        result.setMessage("成功");
        result.setData(data);
        return result;
    }

    public static Result success(){
        return success(null);
    }

    public static Result fail(Integer code, String message){
        Result result = new Result();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
}

接下来我们来测试一下,这里我使用 postman 工具测试:

请求结果成功返回的情形:

请求结果失败的返回情形:

四、AOP 的使用

使用AOP统一处理请求日志。

4.1、什么是 AOP
1.AOP是一种编程方式
    与语言无关,是一种程序设计思想
    面向切面(AOP)Aspect Oriented Programming
    面向对象(OOP)Object Oriented Programming
    面向过程(POP)Procedure Oriented Programming

2.面向过程到面向对象
    面向过程:假如下雨了,我打开了雨伞
    面向对象:天气->下雨,我->打

3.换个角度看世界,换个姿势处理问题

4.将通用逻辑从业务逻辑中分离出来

通过一个简单的流程图,演示如何使用AOP去处理一个请求:

提取执行相同的代码为一个切面:

4.2、如何使用AOP

POM.xml 文件中,添加 aop 的依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

编写一个切面通知类

package com.xmlvhy.girl.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * Author: 小莫
 * Date: 2019-01-25 10:57
 * Description:<描述>
 */
//定义一个切面
@Aspect
@Component
@Slf4j
public class HttpAspect {

    //定义切点
    @Pointcut("execution(public * com.xmlvhy.girl.controller.GirlController.*(..))")
    public void log(){
    }

    //前置通知
    //@Before("execution(public * com.xmlvhy.girl.controller.GirlController.*(..))")
    @Before("log()")
    public void doBefore(JoinPoint join){
        //url
         ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        log.info("url= {}",request.getRequestURL());
        //method
        log.info("method= {}",request.getMethod());
        //请求的ip
        log.info("ip= {}",request.getRemoteAddr());
        //类方法
        log.info("class_method= {}", join.getSignature().getDeclaringTypeName() + "." +join.getSignature().getName());
        //参数
        log.info("args= {}",join.getArgs());
        log.info("==============doBefore 所请求接口的方法执行之前执行============");
    }

    //后置通知
    //@After("execution(public * com.xmlvhy.girl.controller.GirlController.*(..))")
    @After("log()")
    public void doAfter(){
        log.info("==============doAfter 所请求接口的方法执行之后执行============");
    }

    //@AfterReturning可修饰AfterReturning增强处理,AfterReturning增强处理将在目标方法正常完成后被织入
    @AfterReturning(pointcut = "log()",returning = "object")
    public void doAfterReturning(Object object){
        log.info("response= {}",object);
        log.info("==============doAfterReturning 所请求接口的方法执行之后返回结果之前执行============");
    }
}
4.3、常用的注解说明
@Aspect:声明当前类是一个切面处理类
@Component:声明当前类是一个Bean,由Spring的IOC容器进行管理
@Pointcut:声明需要处理的切点

spring aop 通知(advice)分成五类:

@Before:前置通知[Before advice]:在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常。

@AfterReturning:正常返回通知[After returning advice]:在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行。
@AfterThrowing:异常返回通知[After throwing advice]:在连接点抛出异常后执行。

@After:返回通知[After (finally) advice]:在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。

@Around:环绕通知[Around advice]:环绕通知围绕在连接点前后,比如一个方法调用的前后。这是最强大的通知类型,能在方法调用前后自定义一些操作。环绕通知还需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行。
4.4、定义一个接口
@GetMapping(value = "girls")
public List<Girl> girlList(){
    log.info("get girlList");
    return girlRepository.findAll();
}

使用 postman 访问一下,查看一下打印出的日志:

通过日志,我们可以清晰的看到相关方法的执行先后顺序。

五、全局异常处理
5.1、为什么要定义异常处理?
1、在框架层面封装checked exception,将其转化为unchecked exception,避免开发过程中编写繁冗的try...catch代码。

2、业务层面的开发,根据程序代码职责定义不同的RuntimeException(它就是unchecked exception,一般定义为RuntimeException的子类)

3、通过前两个观点,系统中自定义的异常将只存在unchecked exception,系统只在于客户端交换数据的上层,设置统一异常处理机制,并将一些异常转化为用户所能理解的信息传达给用户。

4、其他如业务层,数据持久层,等底层只负责将异常抛出即可,但要注意不要丢失掉异常堆栈(这一点是初学者容易犯的一个错误)。
5.2、什么是异常处理?

如果不加异常处理的话,程序出错了,用户可能不知道是啥原因。但加上异常处理后,用户能最快时间定位错误信息。例如,当一个SpringBoot 程序出现异常时,会默认的给出我们一个异常提示页面:Whitelabel Error Page

但如果我们想要一些相对较友好的提示信息或页面,那么就需要我们进行全局的异常处理了。

  • 定义错误码页面
  • 定义异常通知
5.3、定义错误码页面的方式

创建错误状态码.html页面,放在templates/error目录中,当发生错误时会自动到该目录下查找对应的错误页面。

例如可以创建如4xx.html或5xx.html页面,用来匹配所有该类型的错误(会先进行精确匹配)

5.4、定义异常通知的方式

这里我们做一个简单的用例,获取某个人的年龄并判断,小于10 ,返回“你应该在上小学”,大于10且小于16 ,返回“你可能在上初中”。

我们先封装异常返回信息类:

定义一个枚举类:

package com.xmlvhy.girl.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * Author: 小莫
 * Date: 2019-01-25 18:59
 * Description:<描述>
 */
@Getter
@AllArgsConstructor
public enum ResultEnum {
    UNKNOWN_ERROR(-1,"未知错误"),
    SUCCESS(100,"成功"),
    PRIMARY_SCHOOL(100,"你可能在上小学"),
    MIDDLE_SCHOOL(101,"你可能在上初中"),
    ;
    private Integer code;
    private String message;
}

定义一个自定义异常类:

package com.xmlvhy.girl.exception;

import com.xmlvhy.girl.enums.ResultEnum;
import lombok.Data;

/**
 * Author: 小莫
 * Date: 2019-01-25 18:36
 * Description:<描述>
 */
@Data
public class GirlException extends RuntimeException {
    private Integer code;

    public GirlException(ResultEnum resultEnum) {
        super(resultEnum.getMessage());
        this.code = resultEnum.getCode();
    }
}

这里错误码以及异常信息,我们都统一定义在定义的枚举类中,这样看起来会比较清爽!另外,自定义的异常类,需要继承的RuntimeException 类而不是Exception 类,原因是:springboot 中只对 RuntimeException 类型进行捕获。

使用自定义异常:

@Override
public void getAge(Integer id) throws Exception {
     if (girlRepository.findById(id).isPresent()) {
        Girl girl = girlRepository.findById(id).get();
         log.info("girl= {}",girl);
        Integer age = girl.getAge();
        if (age < 10) {
             log.info("你还在上小学吧");
            //throw new Exception("你还在上小学吧!");
            //throw new GirlException(100,"你还在上小学吧!");
            throw new GirlException(ResultEnum.PRIMARY_SCHOOL);
        }else if(age > 10 && age < 16){
             log.info("你可能在上中学");
            //throw new Exception("你可能在上初中!");
            //throw new GirlException(101,"你可能在上初中!");
            throw new GirlException(ResultEnum.MIDDLE_SCHOOL);
         }
    }
}

定义一个全局异常处理类:

package com.xmlvhy.girl.exception;

import com.xmlvhy.girl.entity.Result;
import com.xmlvhy.girl.util.ResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * Author: 小莫
 * Date: 2019-01-25 16:33
 * Description:<描述>
 */
//定义该类为全局异常捕获类
@ControllerAdvice
@Slf4j
public class ExceptionHandle {

    //标记要捕获的异常
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result exceptionHandle(Exception e){
        //判断异常是否是自定义 GirlException 异常的一个实例
        if (e instanceof GirlException) {
            GirlException girlException = (GirlException) e;
            //log.info("[自定义异常] {}",girlException);
            return  ResultUtil.fail(girlException.getCode(),girlException.getMessage());
        }else{
            log.info("[系统异常] {}", e);
            return ResultUtil.fail(-1, "未知错误");
        }
    }
}

Controller 中我们定义一个接口,来验证一下结果:

@GetMapping(value = "/girls/getAge/{id}")
public void getAge(@PathVariable("id") Integer id) throws Exception {
    girlService.getAge(id);
}

首先我们先插入几条数据到数据库中:

同样使用 postman 工具来测试一下:

年龄小于10的情况:

年龄大于10小于16的情况:

出现系统异常而非自定义异常的情况:

以上,则完成全局异常的处理。

参考链接:
https://segmentfault.com/a/1190000008752288

本文涉及的相关 获取源码

类似文章

  1. SpringCloud入门系列之服务链路追踪Sleuth&Zipkin
  2. SpringCloud入门系列之微服务之间的通信
  3. SpringCloud入门系列之API网关
  4. SpringCloud入门系列之配置中心
  5. SpringCloud入门系列之Eureka注册中心

评论区

| 0 评论

还没有评论,快来抢沙发吧!