Day 17 - Spring Boot 例外处理

经过上一篇Day 16 - Spring Boot 资料验证的功能实作後,我们的业务逻辑层需要处理的判断就变少了,可以让我们的程序码更加的简洁,但还是有其他的问题,就是我们控制器层与业务逻辑层耦合的问题。

当我们在业务逻辑层遇到不理想的状况时,还是会把资料回传给呼叫的方法,再由呼叫的方法进行判断,这样会让每个呼叫这个业务逻辑的方法,都需要判断一次回传的内容,所以实务上我们只会让业务逻辑层回传完整且正确执行後的理想结果其他不理想的状况都直接抛出例外,由ExceptionHandler 统一处理。

统一例外处理

当我们要处理例外的时候,一般情况下都会使用try-catch 语法,但就像上面所说的,我们的业务逻辑方法只想要处理理想的结果,而Spring 有提供了一个非常方便的例外处理方案,也就是利用@ControllerAdvice@RestControllerAdvice ****注释。

透过基於剖面技术实现的@ControllerAdvice@RestControllerAdvice ****注释,可以对例外进行统一处理预设对所有的Controller 有效,若想要限定生效范围则可以使用以下方法。

  1. 指定注释 :

    // Target all Controllers annotated with @RestController
    @ControllerAdvice(annotations = RestController.class)
    public class ExampleAdvice1 {}
    
  2. 指定Package :

    // Target all Controllers within specific packages
    @ControllerAdvice("org.example.controllers")
    public class ExampleAdvice2 {}
    
  3. 指定类型 :

    // Target all Controllers assignable to specific classes
    @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
    public class ExampleAdvice3 {}
    

当我们使用@ControllerAdvice@RestControllerAdvice ****注释开启了对全域例外的捕捉,接下来就只需要做一件事情,那就是使用@ExceptionHandler 注释定义捕捉例外的类型并对指定类型的例外进行统一处理,使用方法就跟下面展示的差不多。

@ControllerAdvice
public class ExampleAdviceDemo {

	// 统一处理NullPointerException 例外
	@ExceptionHandler(NullPointerException.class)
	public String handleNullPointerException() {
		// 忽略
	}

	// 统一处理阵列相关例外,如索引超出范围、资料型别不相容等。
	@ExceptionHandler({ArrayIndexOutOfBoundsException.class, ArrayStoreException.class})
	public String handleArrayException() {
		// 忽略
	}

	// 统一处理其他非上述方法提及之例外
	@ExceptionHandler(Exception.class)
	public String handleException() {
		// 忽略
	}	
}

实作

自定义例外

在实务上,我们可以自己定义例外的类别,为各种状况提供客制化的例外类别。

ServiceException

先定义一个ServiceException,再让其他自定义的例外类别去继承,这样使用@ExceptionHandler ****接例外的时候比较方便。

package com.example.demo.service.ex;

public class ServiceException extends RuntimeException {

	// 略...

}

其他自定义例外类别

package com.example.demo.service.ex;

public class UsernameDuplicateException extends ServiceException {

	// 略...

}
package com.example.demo.service.ex;

public class PasswordNotMatchException extends ServiceException {

	// 略...

}
package com.example.demo.service.ex;

public class InsertException extends ServiceException {

	// 略...

}
package com.example.demo.service.ex;

public class MemberAccountNotFoundException extends ServiceException {

	// 略...

}
package com.example.demo.service.ex;

public class MemberNotFoundException extends ServiceException {

	// 略...

}

修改业务逻辑层的注册方法

@Service
public class MemberAccountServiceImpl implements MemberAccountService {

	// 略...
	
	@Override
	public MemberAccountVO login(MemberAccount memberAccount) {
		// TODO Auto-generated method stub
		// 检查帐号是否存在
		MemberAccount data = memberAccountDao.findMemberAccountByUsername(memberAccount.getUsername());
		if(data == null) throw new MemberAccountNotFoundException("帐号或密码错误");

		// 使用资料库盐值对输入密码进行加密
		String md5Password = getMd5Password(memberAccount.getPassword(), data.getSalt());

		// 比对密码是否相等
		if(!md5Password.equals(data.getPassword())) throw new PasswordNotMatchException("帐号或密码错误");
		
		// 取得对应Member 资料
		Member member = memberService.findMemberByMa_id(data.getId());
		if(member == null) throw new MemberNotFoundException("帐号或密码错误");
		
		// 组合资料为MemberAccountVO
		MemberAccountVO memberAccountVO = new MemberAccountVO();
		memberAccountVO.setUsername(memberAccount.getUsername());
		memberAccountVO.setName(member.getName());
		return memberAccountVO;
	}

	@Override
	public void register(MemberAccountVO memberAccountVO) {
		// TODO Auto-generated method stub
		// 验证栏位是否填写及格式
		if(!memberAccountVO.getPassword().equals(memberAccountVO.getCheckPassword())) throw new PasswordNotMatchException("两次输入密码不相符");
		
		// 检查帐号是否重复注册
		MemberAccount data = memberAccountDao.findMemberAccountByUsername(memberAccountVO.getUsername());
		if(data != null) throw new UsernameDuplicateException("该帐号已被使用");
		
		// 产生盐值
		String salt = UUID.randomUUID().toString().toUpperCase().replaceAll("-", "");
		
		// 密码加密
		String md5Password = getMd5Password(memberAccountVO.getPassword(), salt);

		// 新增MemberAccount 资料
		MemberAccount memberAccount = new MemberAccount();
		memberAccount.setUsername(memberAccountVO.getUsername());
		memberAccount.setPassword(md5Password);
		memberAccount.setSalt(salt);
		memberAccount.setCreate_by(memberAccountVO.getUsername());
		memberAccount.setUpdate_by(memberAccountVO.getUsername());
		Integer id = memberAccountDao.insert(memberAccount);
		if(id == 0) throw new InsertException("新增会员帐号时发生错误");

		// 新增Member 资料
		Member member = new Member();
		member.setMa_id(String.valueOf(id));
		member.setName(memberAccountVO.getName());
		member.setCreate_by(memberAccountVO.getUsername());
		member.setUpdate_by(memberAccountVO.getUsername());
		Integer result = memberService.insert(member);
		if(result == 0) throw new InsertException("新增会员资料时发生错误");
	}

	// 略...

}

建立统一处理例外的控制器

package com.example.demo.controller;

import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.example.demo.service.ex.ServiceException;

@ControllerAdvice
public class ExceptionHandlerController {

	@ExceptionHandler({BindException.class})
	public String handleBindException(
			Throwable e, 
			RedirectAttributes redirectAttributes) {
		
		BindingResult results = ((BindException) e).getBindingResult();

		// 取得第一个例外讯息
		String message = results.getFieldErrors().get(0).getDefaultMessage();
		redirectAttributes.addFlashAttribute("MESSAGE", message);

		// 取得全部例外讯息
		// for(FieldError er :results.getFieldErrors()) {
		// 	System.err.println(er.getDefaultMessage());
		// 	System.err.println(er.getField());
		// }
		
		return "redirect:login";
	}
	
	@ExceptionHandler({ServiceException.class, Exception.class})
	public String handleServiceException(
			Throwable e, 
			RedirectAttributes redirectAttributes) {

		redirectAttributes.addFlashAttribute("MESSAGE", e.getMessage());
		return "redirect:login";
	}
	
}

Github

新增例外处理机制并将任何不符合预期情况改为抛出例外

参考网站

Controller Advice


<<:  Day 18:501. Find Mode in Binary Search Tree

>>:  Day 19 AWS云端实作起手式第九弹 让开机器变得很自动自发Auto Scaling最後一击

第六天:首次启动设定

若是您选择以软件包或 Docker 这种 On Premises 的安装方式安装在本机电脑的话,那首...

部署 Kolla-Ansible 使用 External Ceph

在部署 Kolla-Ansible 时,虽然能够同时部署 Ceph Cluster,但是在一些情况下...

【D21】制作讯号灯#5:使用三大法人制作外资讯号灯

前言 制作了加权指数的,这次制作三大法人-外资的讯号灯,本次会做多单还是空单、留仓数量是否增加、留仓...

当火灾发生时,势必要分流人群,让各个出口可以有效运用,让人群疏散

分散流量 前面的章节,都是一台EC2或个别的Container去执行应用程序,虽然网站可以正常运作,...

Leetcode 挑战 Day 06 [66. Plus One]

66. Plus One 今天这一题相对单纯、简单一些,但当中也有一些小技巧和观念,还是蛮值得一看的...