基于前几篇写的springboot环境搭建,引入了mybatis、generator等相关依赖。
springboot搭建一个后台管理(一):环境配置及搭建 :https://blog.csdn.net/LiLrui/article/details/87916534
springboot搭建一个后台管理(二):最基础的CRUD测试:https://blog.csdn.net/LiLrui/article/details/87920644
这篇以做一个部门管理相关的业务为例,引入全局异常处理、aop、validator校验等,先看下业务关系的图表:
接下来先定义aop相关的代码,我需要在用户发送请求时通过切面来获取信息并打印日志,创建一个HttpAspect类:
/**
* Created by Lilrui.
* Aspect切面来获取请求,这个类丢给spring管理
*/
@Aspect
@Component
@Slf4j
public class HttpAspect {
//需要将切面植入到哪个包/类
@Pointcut("execution(public * com.sansheng.controller..*(..))")
public void print() {
}
//请求之前打印
@Before("print()")
public void doBefore(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// Enumeration<String> enumeration = request.getParameterNames();
// Map<String,Object> parameterMap = new HashMap<String,Object>();
Object[] obj = joinPoint.getArgs();
//添加请求的ip
RequestHolder.add(request);
log.info("url={}", request.getRequestURL());
log.info("method={}", request.getMethod());
log.info("ip={}", request.getRemoteAddr());
log.info("class_method={}{}",joinPoint.getSignature().getName(),"()");
// if (obj.length==1){
// log.info("args={}",joinPoint.getArgs());
// }else {
// for (Object o:obj) {
//
// parameterMap.put(enumeration.nextElement(),o);
//
// }
// log.info("args={}", parameterMap);
// }
}
//请求之后打印
// @After("print()")
// public void doAfter() {
//
// }
//不管成功失败,总是打印
@AfterReturning(returning = "object", pointcut = "print()")
public void doAfterReturning(Object object) {
log.info("response={}", object.toString());
}
以test测试为例,测试下切面是否被植入:用postman工具测试 http://localhost/Test/findAll 接口查询全部
然后看到控制台打印了日志信息
切面的植入是没有问题的,我的业务只需要它在日志中打印信息,如果你需要切面做更多的事情,比如请求之后的处理,就在doAfter()去添加,具体根据自己的业务需要去拓展。
接下来做全局异常捕获处理,做这个是为了把异常做统一监听并处理,而不是在代码中到处的 try catch捕获, 这样会显的代码非常的臃肿,而且不利于维护。java中有一个叫 RuntimeException 的类,这个类是运行时的异常类,也就是它并不强制性的要求我们去捕获,而是在我们觉的某个业务运行时可能会出现异常,比如:我传入了一个空字段。这时系统会报运行时异常。我们可以利用它来自定义我们的异常。
我们需要先创建一个自定义的枚举类,来管理我们参数出异常的信息:
/**
* Created by Lilrui.
*/
@Getter
public enum ErrorEnum {
PARAMETER_VALIDATION_ERROR(10001,"参数不合法"),
USER_IS_NOT_EXIST(10002,"用户不存在"),
DEPT_IS_NOT_EXIST(10003,"部门不存在"),
DEPTNAME_IS_EXIST(10004,"同一层级下存在相同名称的部门"),
UNKNOW_ERROR(10004,"系统异常:未知错误")
;
private Integer code;
private String msg;
ErrorEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
里面的枚举根据自己的业务需要去定义。这里先添加部门相关的异常信息,然后我需要把自定义的enum类 跟我们的异常绑定,
怎么绑定,首先要创建一个自定义异常类 BusinessException去继承我们的RuntimeException ,然后把enum给注入到类里:
/**
* Created by Lilrui.
*/
@Setter
@Getter
public class BusinessException extends RuntimeException {
private Integer code;
private ErrorEnum errorEnum;
public BusinessException(ErrorEnum errorEnum) {
super(errorEnum.getMsg());
this.code = errorEnum.getCode();
}
public BusinessException() {
super();
}
public BusinessException(ErrorEnum errorEnum,String message) {
super(message);
this.code = errorEnum.getCode();
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
public BusinessException(Throwable cause) {
super(cause);
}
protected BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
这时候已经绑定好了,接下来需要把异常跟我们的web层进行结合,因为所有的请求入口都是来自web层,我们需要一个Handle来吸收我们的异常信息。所以要创建一个ExceptionHandle类来绑定web层:
/**
* Created by Lilrui.
*/
@ControllerAdvice
@Slf4j
public class ExceptionHandle {
@ExceptionHandler(value = Exception.class)
@ResponseBody
public JsonResult handle(Exception e) {
Map<String,Object> responseData = new HashMap<>();
if (e instanceof BusinessException) {
BusinessException businessException = (BusinessException) e;
responseData.put("errCode",businessException.getCode());
responseData.put("errMsg",businessException.getMessage());
}else {
log.error("系统异常:", e);
responseData.put("errCode", ErrorEnum.UNKNOW_ERROR.getCode());
responseData.put("errMsg",ErrorEnum.UNKNOW_ERROR.getMsg());
}
return ResultUtil.error(responseData);
}
}
这里我还需要介入一个工具类:BindingResult,用来在web层捕获异常并将所有异常依次返回
/**
* Created by Lilrui.
*/
@Slf4j
public class BindingResultUtil {
//这个类作用是把捕获到的所有校验异常都捕获,并依次输出。返回的可能不止一个异常
public static String toErrMsg(BindingResult bindingResult) {
List<String> errorMsg = Lists.newArrayList();
bindingResult.getAllErrors().forEach(error -> errorMsg.add(error.getDefaultMessage()));
return JSON.toJSON(errorMsg).toString();
}
}
绑定成功了其实还有一个问题,就是我们要把异常信息能正确的返回给客户端,而且是一个统一的格式,就想上篇讲过的,通过自定义JsonResult来 讲数据格式进行统一,我希望如果请求成功,返回一个正常信息,请求失败,则返回异常信息。那么接下来再写一个工具类,来包装JsonResult。
/**
* Created by Lilrui.
* 通过包装JsonResult来保证 正常情况返回数据信息,异常情况返回异常信息
*/
public class ResultUtil {
public static JsonResult success(Object object) {
JsonResult result = new JsonResult(true,"success");
result.setData(object);
return result;
}
public static JsonResult success() {
return success(null);
}
public static JsonResult error(Object object) {
JsonResult result = new JsonResult(false,"fail");
result.setData(object);
return result;
}
}
定义好全局异常跟aop切面后,接下来写部门相关的业务:
添加部门:id自增
name:部门名称不能为空
seq:展示顺序不能为空
remark:备注在200字以内
修改部门:id:传入的id查询不到则报部门不存在异常
name:同一层级下部门名称不能相同
删除部门:id:传入的部门主键查询不到则报部门不存在异常
以上业务都有传入参数的限制,那么先来来定义参数校验,单独把需要校验的参数给封装成一个param类:
/**
* Created by Lilrui.
*这些是我要校验的参数
*/
@Data
public class DeptParam {
private Integer id;
@NotBlank(message = "部门名称不能为空")
private String name;
private Integer parentId;
@NotNull(message = "展示顺序不能为空")
private Integer seq;
@Length(max = 200,message = "备注必须在0~200字以内")
private String remark;
}
然后之前generator已经帮我们生成好了mapper接口,我们这里需要把dao层和sercvice给分开,保证dao只做crud。而sercvice做业务相关操作:
public interface SysDeptMapper {
int deleteByPrimaryKey(@Param("id")Integer id) throws Exception;//根据id删除部门
int insert(SysDept record);//插入一个部门
int insertSelective(SysDept record);//插入一个部门,若只插入了部分字段,只写入插入的值
SysDept selectByPrimaryKey(@Param("id")Integer id) throws Exception;//根据id查询部门
int updateByPrimaryKeySelective(SysDept record);//修改部门,若只修改了部分字段,只更新修改过的值
List<SysDept> selectAllDept();//查询所有部门
int selectNameAndParentId(@Param("parentId") Integer parentId, @Param("name") String name, @Param("id") Integer id);//查询同层级下的部门是否存在
int updateByPrimaryKey(SysDept record);//修改部门
}
然后定义一个接口继承mapper,并加上事务
/**
* Created by Lilrui.
*/
@Service
public class DeptMapperImpl implements SysDeptMapper{
@Resource
private SysDeptMapper sysDeptMapper;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int deleteByPrimaryKey(Integer id)throws Exception {
//id不存在则抛出部门不存在异常
if (null==sysDeptMapper.selectByPrimaryKey(id)){
throw new BusinessException(ErrorEnum.DEPT_IS_NOT_EXIST);
}
return sysDeptMapper.deleteByPrimaryKey(id);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int insert(SysDept dept) {
return sysDeptMapper.insert(dept);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int insertSelective(SysDept dept) {
return sysDeptMapper.insertSelective(dept);
}
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public SysDept selectByPrimaryKey(Integer id) throws Exception{
if (null==sysDeptMapper.selectByPrimaryKey(id)){
throw new BusinessException(ErrorEnum.DEPT_IS_NOT_EXIST);
}
return sysDeptMapper.selectByPrimaryKey(id);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int updateByPrimaryKeySelective(SysDept dept) {
return sysDeptMapper.updateByPrimaryKeySelective(dept);
}
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public List<SysDept> selectAllDept() {
return sysDeptMapper.selectAllDept();
}
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public int selectNameAndParentId(Integer parentId,String name,Integer id) {
return sysDeptMapper.selectNameAndParentId(parentId,name,id);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int updateByPrimaryKey(SysDept dept) {
return sysDeptMapper.updateByPrimaryKey(dept);
}
}
接下来就是写我们的业务代码了:
/**
* Created by Lilrui.
*/
@Service
@Slf4j
public class DeptService {
@Autowired
private DeptMapperImpl deptMapper;
/**
* 保存部门业务
* @param param
*/
public void save(DeptParam param) throws Exception{
if(checkExist(param.getParentId(), param.getName(), param.getId())) {
log.error("同一层级下存在相同的部门:{}",param.getName());
throw new BusinessException(ErrorEnum.DEPTNAME_IS_EXIST);
}
//level,operator暂时写个固定值,后续业务再做开发
SysDept dept = SysDept.builder()
.name(param.getName())
.parentId(param.getParentId())
.seq(param.getSeq())
.remark(param.getRemark())
.level("1")
.operateIp(IpUtil.getRemoteIp(RequestHolder.getIp()))
.operator("admin")
.operateTime(new Date())
.build();
deptMapper.insertSelective(dept);
RequestHolder.remove();
}
/**
* 更新部门业务
*/
public void update(DeptParam param)throws Exception{
if(checkExist(param.getParentId(), param.getName(), param.getId())) {
log.error("同一层级下存在相同的部门:{}",param.getName());
throw new BusinessException(ErrorEnum.DEPTNAME_IS_EXIST);
}
SysDept oldDept =deptMapper.selectByPrimaryKey(param.getId());
SysDept afterDept = SysDept.builder()
.id(param.getId())//更新需传入主键
.name(param.getName())
.parentId(param.getParentId())
.seq(param.getSeq())
.remark(param.getRemark())
.level("0.1.2")
.operateIp(IpUtil.getRemoteIp(RequestHolder.getIp()))
.operator("admin")
.operateTime(new Date())
.build();
deptMapper.updateByPrimaryKeySelective(afterDept);
}
/**
* 查询同一部门下是否有相同的部门
* @param parentId
* @param deptName
* @param deptId
* @return
*/
private boolean checkExist(Integer parentId, String deptName, Integer deptId) {
return deptMapper.selectNameAndParentId(parentId, deptName, deptId) > 0;
}
}
最后写web层,添加controller接口
/**
* Created by Lilrui.
*/
@Controller
@RequestMapping("/dept")
@Slf4j
public class DeptController {
private final DeptService deptService;
private final DeptMapperImpl deptMapper;
/**
* 构造器注入
* @param deptService
* @param deptMapper
*/
public DeptController(DeptService deptService, DeptMapperImpl deptMapper) {
this.deptService = deptService;
this.deptMapper = deptMapper;
}
@PostMapping("/insert")
@ResponseBody
public JsonResult insert(@Valid DeptParam deptParam, BindingResult bindingResult) throws Exception {
//通过注入BindingResult 来获取异常,如果有异常则捕获
if (bindingResult.hasErrors()) {
throw new BusinessException(ErrorEnum.PARAMETER_VALIDATION_ERROR,BindingResultUtil.toErrMsg(bindingResult));
}
deptService.save(deptParam);
return JsonResult.success();
}
@PostMapping("/update")
@ResponseBody
public JsonResult update(DeptParam deptParam)throws Exception{
deptService.update(deptParam);
return JsonResult.success();
}
@PostMapping("/getAll")
@ResponseBody
public JsonResult selectAll() {
return JsonResult.success(deptMapper.selectAllDept());
}
@PostMapping("/getOne")
@ResponseBody
public JsonResult getOne(@Param(value = "id")Integer id) throws Exception{
return JsonResult.success(deptMapper.selectByPrimaryKey(id));
}
@PostMapping("/coutNameAndParentId")
@ResponseBody
public JsonResult selectNameAndParentId(@Param(value = "parentId") Integer parentId,
@Param(value = "name") String name,
@Param(value = "id") Integer id) {
return JsonResult.success(deptMapper.selectNameAndParentId(parentId, name, id));
}
@PostMapping("/delete")
@ResponseBody
public JsonResult delete(@Param(value = "id") Integer id)throws Exception{
deptMapper.deleteByPrimaryKey(id);
return JsonResult.success();
}
}
最后我们来测试下我们写的业务:使用postman 测试添加部门业务:http://localhost/dept/insert?name=测试&parentId=1&seq=2&remark=1
看数据库是否有这条信息:
这是正常添加,接下来我们添加几个空字段试试:
然后我再添加一条重复的数据:
接下来测试修改业务:http://localhost/dept/update?id=17&name=测试233&parentId=1&seq=2&remark=测试2333
查看数据库是否修改成功:
然后我们重复修改试试:
我们查询一个部门:http://localhost/dept/getOne?id=17
输入一个不存在的部门id:
这些都是我们自定义的异常,最后测试下系统异常,我在代码里手动加个错误:
然后测试这个接口:
以上就是我们的全局异常处理,字段校验,以及aop测试。