首先先来看一组代码:
Controller:
@RequestMapping public ModelAndView doCreate(HttpServletRequest request, Mapout, Events event, String gmtStartStr, String gmtEndStr, String invitedAccountId){ eventsService.initGmt(event, gmtStartStr, gmtEndStr); event.setUid(getSessionUser(request).getUid()); Integer id = eventsService.saveEvent(event); if(id!=null && id.intValue()>0){// eventsService.appendJoiner(id, invitedAccountId); return new ModelAndView("redirect:index.do"); } return new ModelAndView("/events/create"); }
Service:
@Override public Integer saveEvent(Events event) { if(event == null){ return null; } if(Strings.isNullOrEmpty(event.getName())){ return null; } event.setStatusPublic(event.getStatusPublic()==null?Events.STATUS_DEFAULT:event.getStatusPublic()); eventsMapper.save(event); return event.getId(); }
这组代码应该算是比较典型的 controller 调用 service 实现业务功能的代码,以前的系统里一直也是这么干的,也有确实没有遇到什么特别严重的问题,但是在错误返回上总是会有那么点小纠结,有时候,错误是由于参数引起的,有时候是由于数据库操作引起的,在controller层其实不能特别准确的判断,特别是那些复杂的 service 方法,错误往往产生于众多逻辑中的一个,如果只是返回 null,controller 就只能知道方法是执行成功还是失败,但不知道失败的细节。另一个问题是 null 值判断被移到了 controller 里,这导致 controller 参与了业务,与 controller 应有的职责不符。
那么如何改进呢?
针对错误细节,我们可以把一个业务方法执行过程中,不可继续的环节认为是一种错误,只不过这种错误是开发人员自己可控的逻辑错误。错误在软件系统里也叫异常,所以,用异常来改进上述代码:
Controller:
@RequestMapping public ModelAndView doCreate(HttpServletRequest request, Mapout, Events event, String gmtStartStr, String gmtEndStr, String invitedAccountId){ eventsService.initGmt(event, gmtStartStr, gmtEndStr); event.setUid(getSessionUser(request).getUid()); try{ Integer id = eventsService.saveEvent(event); return new ModelAndView("redirect:index.do"); }catch(Exception e){ out.put("error",e.getMessage()); return new ModelAndView("/events/create"); } }
Service:
@Override public Integer saveEvent(Events event) throws Exception{ if(event == null){ throw new Exception("event can not be null."); } if(Strings.isNullOrEmpty(event.getName())){ throw new Exception("event.name can not be null."); } event.setStatusPublic(event.getStatusPublic()==null?Events.STATUS_DEFAULT:event.getStatusPublic()); eventsMapper.save(event); if(event.getId() ==null || event.getId()<=0){ throw new Exception("event save failure."); } return event.getId(); }
以上改进的好处是在 controller 层很容易知道 service 出错的细节,同时也居然了处理 null 值判断问题,也可以在 controller 层做统一异常处理,比如跳转到错误页面,或返回错误的 JSON 等。
但是,实际的项目中,service 层业务并不总是这么简单,当 service 层中使用的工具或者类中本身就有异常抛出的时候,因为抛出的是 Exception,所以其他的异常信息被忽略了,在 controller 里仍然不能准确把握要处理的异常,这可能会导致前端页面无法正确提示不同的异常信息。
既然要区分不同的异常,那么把自己业务里的异常都用自定义异常类 ServiceException 来抛出不就可以解决问题了吗,代码改进如下:
Controller:
@RequestMapping public ModelAndView doCreate(HttpServletRequest request, Mapout, Events event, String gmtStartStr, String gmtEndStr, String invitedAccountId){ eventsService.initGmt(event, gmtStartStr, gmtEndStr); event.setUid(getSessionUser(request).getUid()); try{ Integer id = eventsService.saveEvent(event); return new ModelAndView("redirect:index.do"); }catch(ServiceException e){ out.put("error",e.getMessage()); return new ModelAndView("/events/create"); } }
Service:
@Override public Integer saveEvent(Events event) throws ServiceException{ if(event == null){ throw new ServiceException("event can not be null."); } if(Strings.isNullOrEmpty(event.getName())){ throw new ServiceException("event.name can not be null."); } try{ event.setStatusPublic(event.getStatusPublic()==null?Events.STATUS_DEFAULT:event.getStatusPublic()); }catch(OtherException e){ throw new ServiceException("set status failure.", e); } eventsMapper.save(event); if(event.getId() ==null || event.getId()<=0){ throw new Exception("event save failure."); } return event.getId(); }
嗯,到这里为止,代码看上去已经比较优雅了,让我们再来找找茬,首先是参数判定,虽然现在是用自己的异常类来做了处理,但 service 参数传递的错误应属于开发调用时就没正确调用导致,级别应属于Error级别,这种级别的错误应在开发时就被处理掉,正常情况下不应有这种情况发生,也不应该与业务上的逻辑错误归为一类,而 jdk 本身提供了参数较验的异常 IllegalArgumentException,这个异常继承自 RuntimeException,我们可以好好利用一下这个异常类。
另外一方面,现在 ServiceException 返回的错误信息对用户来说并不友好,应改进成用户可以理解的语言,比如改成国际化配置文件中的 key,这样可以直接在 controller 层得到有设定好的错误提示信息,也可以在 controller 层直接抛提供给前端,让前端转成国际化结果。
代码再次改进:
Controller:
@Resource private MessageSource messageSource; @RequestMapping public ModelAndView doCreate(HttpServletRequest request, Mapout, Events event, String gmtStartStr, String gmtEndStr, String invitedAccountId, Locale locale){ eventsService.initGmt(event, gmtStartStr, gmtEndStr); event.setUid(getSessionUser(request).getUid()); try{ Integer id = eventsService.saveEvent(event); return new ModelAndView("redirect:index.do"); }catch(ServiceException e){ out.put("error",messageSource.getMessage(e.getMessage(), null, locale)); return new ModelAndView("/events/create"); } }
Service:
@Override public Integer saveEvent(Events event) throws ServiceException{ if(event == null){ throw new IllegalArgumentException("event can not be null."); } if(Strings.isNullOrEmpty(event.getName())){ throw new IllegalArgumentException("event.name can not be null."); } try{ event.setStatusPublic(event.getStatusPublic()==null?Events.STATUS_DEFAULT:event.getStatusPublic()); }catch(OtherException e){ throw new ServiceException("EVENT_SET_STATUS_FAILURE", e); } eventsMapper.save(event); if(event.getId() ==null || event.getId()<=0){ throw new Exception("EVENT_SAVE_FAILURE"); } return event.getId(); }
至此,代码优化暂时告一段落,代码的改进是无止境的,我们不应停留在原有的模式上,要不断寻求新的改进方法,而在改进过程中,更应注重 JDK,JAVA 本身所提供了各类工具和方法。
相关连接:
关于 null 判断的问题,importNews 上有一篇文章写得很好: