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》课程,请先看入门课。
- 使用@Valid表单验证
- 使用AOP处理请求
- 统一异常处理
- 单元测试
第2章 Web进阶
SpringBoot进阶中web方面的内容
2-0 表单验证
# 克隆项目代码
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);
}
表单验证:
在实体Bean里需要校验的字段上面添加注解
@Min(value = 18, message = "未成年少女禁止入内") private Integer age; @NotNull(message = "金额必传") private Integer money;
在接收参数时,使用@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统一处理请求日志
AOP是一种编程方式
- 与语言无关,是一种程序设计思想
- 面向切面(AOP)Aspect Oriented Programming
- 面向对象(OOP)Object Oriented Programming
- 面向过程(POP)Procedure Oriented Programming
面向过程到面向对象
- 面向过程:假如下雨了,我打开了雨伞
- 面向对象:天气->下雨,我->打伞
换个角度看世界,换个姿势处理问题
面向对象:将需求功能垂直划分为不同的并且相对独立的,封装为良好的类并且让其有自己的行为
面向切面:利用横切技术将面向对象的庞大体系进行水平切割,并且将影响到多个类的公共行为进行封装成一个可重用的模块(切面)
将通用逻辑从业务逻辑中分离出来
AOP示例:

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

登录授权访问
使用AOP流程:
添加POM文件
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
建立处理文件
@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 统一异常处理(上)
什么是异常处理?
如果不加异常处理的话,程序出错了,用户可能不知道是啥原因。
加上处理后,比如加上友好信息等用户能知道错在哪。
为什么要统一异常处理?
- 在框架层面封装checked exception,将其转化为unchecked exception,避免开发过程中编写繁冗的try…catch代码。
- 业务层面的开发,根据程序代码职责定义不同的RuntimeException(它就是unchecked exception,一般定义为RuntimeException的子类)
- 通过前两个观点,系统中自定义的异常将只存在unchecked exception,系统只在于客户端交换数据的上层,设置统一异常处理机制,并将一些异常转化为用户所能理解的信息传达给用户。
- 其他如业务层,数据持久层,等底层只负责将异常抛出即可,但要注意不要丢失掉异常堆栈(这一点是初学者容易犯的一个错误)。
统一异常处理返回格式:
{
"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 课程总结
- 使用@Valid表单验证
- 使用AOP处理请求
- 统一异常处理
- 单元测试
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 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" 转载请保留原文链接及作者。