810-Spring Boot进阶之Web进阶

https://www.imooc.com/learn/810

简介:《2小时学习Spring Boot》后续进阶课程,主要讲述了Spring Boot针对Web方面的相关技巧 讲师实战课程《Spring Boot微信点餐系统》 http://coding.imooc.com/class/117.html 《Spring Cloud微服务实战》 http://coding.imooc.com/class/187.html 均已上线

第1章 课程介绍

课程介绍

1-1 课程介绍

本课程紧接着《2小时学会SpringBoot》课程,请先看入门课。

  1. 使用@Valid表单验证
  2. 使用AOP处理请求
  3. 统一异常处理
  4. 单元测试

第2章 Web进阶

SpringBoot进阶中web方面的内容

2-0 表单验证

项目源码:https://gitee.com/liaoshixiong/girl

# 克隆项目代码
git clone git@gitee.com:liaoshixiong/girl.git
# 切换分支
git checkout -b web-2 web-2

当Girl的参数特别多的时候,可以使用Java实体类接收参数。

/**
 * 添加一个女生
 */
@PostMapping(value = "/girls")
public Girl girlAdd(Girl girl) {
    return girlRepository.save(girl);
}

表单验证:

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

    @Min(value = 18, message = "未成年少女禁止入内")
    private Integer age;
    @NotNull(message = "金额必传")
    private Integer money;
  2. 在接收参数时,使用@Valid注解进行校验

    @PostMapping(value = "/girls")
    public Result<Girl> girlAdd(@Valid Girl girl, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            return ResultUtil.error(ResultUtil.RESPCODE_ERROR_PARAM,bindingResult.getFieldError().getDefaultMessage());
        }
        // ...
    }

2-1 使用AOP处理请求(上)

AOP统一处理请求日志

  1. AOP是一种编程方式

    • 与语言无关,是一种程序设计思想
    • 面向切面(AOP)Aspect Oriented Programming
    • 面向对象(OOP)Object Oriented Programming
    • 面向过程(POP)Procedure Oriented Programming
  2. 面向过程到面向对象

    • 面向过程:假如下雨了,我打开了雨伞
    • 面向对象:天气->下雨,我->打伞
  3. 换个角度看世界,换个姿势处理问题

    • 面向对象:将需求功能垂直划分为不同的并且相对独立的,封装为良好的类并且让其有自己的行为

    • 面向切面:利用横切技术将面向对象的庞大体系进行水平切割,并且将影响到多个类的公共行为进行封装成一个可重用的模块(切面)

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

AOP示例:

面向对象处理流程

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

面向切面处理流程

登录授权访问

使用AOP流程:

  1. 添加POM文件

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
  2. 建立处理文件

    @Aspect
    @Component
    public class HttpAspect {
        @Before("execution(public * com.imooc.controller.GirlController.girlList(...))")
        public void log() {
            System.out.println(11111);
        }
    }

2-2 使用AOP处理请求(中)

编写切面通知执行方法

@Aspect
@Component
public class HttpAspect {
    /** 日志 */
    private final static Logger LOGGER = LoggerFactory.getLogger(HttpAspect.class);

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

    /** 前置通知 */
    @Before("log()")
    public void doBefore(JoinPoint joinPoint){
        ServletRequestAttributes attributes =  (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // url
        LOGGER.info("url={}", request.getRequestURI());
        // method
        LOGGER.info("method={}", request.getMethod());
        // ip
        LOGGER.info("ip={}", request.getRemoteAddr());
        // 类方法
        LOGGER.info("class_method={}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        // 参数
        LOGGER.info("args={}", joinPoint.getArgs());
    }
}

@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方法)还是中断执行。

2-3 使用AOP处理请求(下)

/** 后置通知 */
@After("log()")
public void doAfter(){
    LOGGER.info("方法执行之后执行");
}
/** 正常返回通知 */
@AfterReturning(returning = "object", pointcut = "log()")
public void doAfterReturning(Object object){
    // LOGGER.info("response={}", object.toString());
}

2-4 统一异常处理(上)

什么是异常处理?

如果不加异常处理的话,程序出错了,用户可能不知道是啥原因。
加上处理后,比如加上友好信息等用户能知道错在哪。

为什么要统一异常处理?

  1. 在框架层面封装checked exception,将其转化为unchecked exception,避免开发过程中编写繁冗的try…catch代码。
  2. 业务层面的开发,根据程序代码职责定义不同的RuntimeException(它就是unchecked exception,一般定义为RuntimeException的子类)
  3. 通过前两个观点,系统中自定义的异常将只存在unchecked exception,系统只在于客户端交换数据的上层,设置统一异常处理机制,并将一些异常转化为用户所能理解的信息传达给用户。
  4. 其他如业务层,数据持久层,等底层只负责将异常抛出即可,但要注意不要丢失掉异常堆栈(这一点是初学者容易犯的一个错误)。

统一异常处理返回格式:

{
    "code": 1,
    "msg": "金额必传",
    "data": null
}
{
    "code": 1,
    "msg": "成功",
    "data": {
        "id": 20,
        "cupSize": "B",
        "age": 25,
        "money": 1.2
    }
}

:arrow_up_small:

public class Result<T> {
    /** 响应编号 */
    private Integer respCode;
    /** 响应消息 */
    private String respMsg;
    /** 具体的内容 */
    private T data;
    // ...getter and setter...
}
/**
 * 添加一个女生
 * @return
 */
@PostMapping(value="/girls")
public Result<Girl> girlAdd(@Valid Girl girl, BindingResult bindingResult) {
    Result result = new Result();
    if(bindingResult.hasErrors()) {
        result.setCode(1);
        result.setMsg(bindingResult.getFieldError().getDefaultMessage());
        return result;
    }

    girl.setCupSize(girl.getCupSize());
    girl.setAge(girl.getAge());

    result.setCode(0);
    result.setMsg("成功");
    result.setData(girlRepository.save(girl));
    return result;
}

优化:

/**
 * http请求返回的实体Bean工具类
 */
public class ResultUtil {
    /** 成功 */
    public final static Integer RESPCODE_SUCCESS = 200;
    /** 请求参数错误 */
    public final static Integer RESPCODE_ERROR_PARAM = 300;
    /** 系统内部业务错误 */
    public final static Integer RESPCODE_ERROR_SERVICE = 400;
    /** 系统内部异常 */
    public final static Integer RESPCODE_ERROR_EXECEPTION = 500;
    /** 执行成功,返回参数 */
    public  static Result success(Object object){
        Result result = new Result();
        result.setRespCode(ResultUtil.RESPCODE_SUCCESS);
        result.setRespMsg("成功");
        result.setData(object);
        return result;
    }
    /** 执行成功,无返回参数 */
    public static Result success(){
        return success(null);
    }
    /** 执行错误 */
    public static Result error(Integer code,String msg){
        Result result = new Result();
        result.setRespCode(code);
        result.setRespMsg(msg);
        return result;
    }
}
// 成功返回
return ResultUtil.success(girlRepository.save(girl));
// 失败返回
return ResultUtil.error(bindingResult.getFieldError().getDefaultMessage());

例Ⅰ:

获取某女生的年龄并判断

小于10,返回“应该在上小学”

大于10且小于16,返回“可能在上初中”

可能比较容易想到的解决方案是:通过标识来传递信息,比如:

public Integer getAge1(Integer id) {
    Girl girl = girlRepository.findOne(id);
    Integer age = girl.getAge();
    if (age < 10) {
        // 返回“你还在上小学吧”
        return 1;
    } else if (10 < age && age < 16) {
        // 返回“你可能在上初中”
        return 2;
    }
    // 如果 > 16岁,加钱
    // ...
    return 0;
}

2-5 统一异常处理(中)

使用统一异常处理后,则为另一种解决方式

步骤一:自定义异常

public class RespException extends RuntimeException {
    /** 响应编号 */
    private Integer respCode;
    public RespException(Integer respCode, String message) {
        super(message);
        this.respCode = respCode;
    }
    // ...getter and setter...
}

步骤二:全局异常统一处理类

@ControllerAdvice
public class ExceptionHandle {
    private final static Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class);
    /**
     * 全局异常返回处理
     * @param e 异常
     * @return 处理后的返回结果
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result handle(Exception e) {
        if (e instanceof RespException) {
            RespException respException = (RespException) e;
            return ResultUtil.error(respException.getRespCode(), respException.getMessage());
        }
        LOGGER.error("【系统异常】{}", e);
        return ResultUtil.error(ResultUtil.RESPCODE_ERROR_SERVICE, "未知错误");
    }
}

步骤三:使用自定义异常

public void getAge(Integer id) throws Exception {
    Girl girl = girlRepository.findOne(id);
    Integer age = girl.getAge();
    if (age < 10) {
        // 返回“你还在上小学吧”
        throw new RespException(100, "你还在上小学吧");
    } else if (age > 10 && age < 16) {
        // 返回“你可能在上初中”
        throw new RespException(101, "你可能在上初中");
    }
    // 如果 > 16岁,价钱
    // ...
}

2-6 统一异常处理(下)

使用枚举进行优化响应编码及响应消息,便于统一维护

步骤一:定义枚举类

/**
 * http请求返回的消息响应编号
 */
public enum ResultResp {
    SUCCESS(0, "成功"),
    UNKONW_ERROR(-1, "未知错误"),
    PARAM_ERROR(1, "参数错误"),
    PRIMARY_SCHOOL(100, "你可能还在上小学"),
    MIDDLE_SCHOOL(101, "你可能在上初中");
    private Integer respCode;
    private String respMsg;
    // ...constructor and getter and setter...
}

步骤二:优化自定义异常

public class RespException extends RuntimeException {
    /** 响应编号 */
    private Integer respCode;
    public RespException(ResultResp resultResp) {
        super(resultResp.getRespMsg());
        this.respCode = resultResp.getRespCode();
    }
    // ...getter and setter...
}

步骤三:全局异常统一处理类

@ControllerAdvice
public class ExceptionHandle {
    private final static Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class);
    /**
     * 全局异常返回处理
     * @param e 异常
     * @return 处理后的返回结果
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result handle(Exception e) {
        if (e instanceof RespException) {
            RespException respException = (RespException) e;
            return ResultUtil.error(respException.getRespCode(), respException.getMessage());
        }
        LOGGER.error("【系统异常】{}", e);
        return ResultUtil.error(ResultUtil.RESPCODE_ERROR_SERVICE, "未知错误");
    }
}

步骤四:使用自定义异常

public void getAge(Integer id) throws Exception {
    Girl girl = girlRepository.findOne(id);
    Integer age = girl.getAge();
    int primarySchool = 10;
    int middleSchool = 16;
    if (age < primarySchool) {
        // 返回“你还在上小学吧”
        throw new RespException(ResultResp.PRIMARY_SCHOOL);
    } else if (age > primarySchool && age < middleSchool) {
        // 返回“你可能在上初中”
        throw new RespException(ResultResp.MIDDLE_SCHOOL);
    }
    // 如果 > 16岁,价钱
    // ...
}

2-7 单元测试

测试Service
测试API

测试Service

@RunWith(SpringRunner.class)
@SpringBootTest
public class GirlServiceImplTest {
    @Autowired
    private GirlService girlService;
    @Test
    public void findOne() throws Exception {
        Girl girl = girlService.findOne(7);
        Assert.assertEquals(new Integer(22), girl.getAge());
    }
}

测试Controller

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class GirlControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    public void listGirl() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/girls"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}
# 项目打包时自动进行单元测试
mvn clean package
# 项目打包时跳过单元测试
mvn clean package -Dmaven.test.skip=true

第3章 课程总结

课程总结

3-1 课程总结

  1. 使用@Valid表单验证
  2. 使用AOP处理请求
  3. 统一异常处理
  4. 单元测试

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 tuyrk@qq.com

文章标题:810-Spring Boot进阶之Web进阶

文章字数:2.6k

本文作者:神秘的小岛岛

发布时间:2019-06-09, 20:59:01

最后更新:2019-11-05, 19:22:10

原始链接:https://www.tuyrk.cn/imooc/810-SpringBoot-Web/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏