SpringMVC体系下各组件的功能边界及重构建议
2018-09-01 05:41:35来源:博客园 阅读 ()
最近在重构后端代码,很多同学对Spring体系下的后端组件如Controller、Service、Repository、Component等认识不够清晰,导致代码里常常会出现Controller里直接使用RestTemplate、直接访问数据库的情况。下面谈谈我对这些组件功能边界的认识,一家之言,欢迎讨论。
1. Controller
Controller是整个后端服务的门面,他向外暴露了可用服务。你不关心Dispatcher、HandleMapping如何作用,但你肯定关心Controller中暴露的接口的HttpMethod、URL Path等。
官方对Controller的说明:
Indicates that an annotated class is a "Controller" (e.g. a web controller).
This annotation serves as a specialization of Component,
allowing for implementation classes to be autodetected through classpath scanning.
It is typically used in combination with annotated handler methods based on the annotation.
Component的一种、会被自动扫描、与RequestMapping合用--其实并没涉及到Controller的功能边界的说明。
虽然官方说明没有涉及,但大量的最佳实践还是告诉我们,Controller只做三件事:
- 校验输入:PathVariable\RequestBody\RequestParam合法性校验
- 业务逻辑:Service层代码调用,并且只调用单个service的单个方法(尽量一行代码搞定),复杂的业务逻辑组装需放在service中
- 控制输出:根据校验、业务逻辑给出合适的response
1.1 输入校验
对于特殊的输入可以用一个if搞定;对于通用输入的校验(如接口的授权校验),可以通过自定义Filter或者自定义切面完成。
自定义Filter示例:
1 @Order(1) 2 @WebFilter(filterName = "authorizationFilter", urlPatterns = "/*") 3 public class AuthorizationFilter implements Filter 4 { 5 @Override 6 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 7 throws IOException, ServletException 8 { 9 HttpServletRequest req = (HttpServletRequest)request; 10 HttpServletResponse res = (HttpServletResponse)response; 11 String path = req.getServletPath(); 12 if (!isWhiteList(path)) 13 { 14 String token = req.getHeader(AUTHORIZATION_HEADER_NAME); 15 if (isValidate(token)) 16 { 17 res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 18 return; 19 } 20 } 21 chain.doFilter(request, response); 22 }
自定义切面示例:
1 @Target(ElementType.METHOD) 2 @Retention(RetentionPolicy.RUNTIME) 3 @Documented 4 public @interface AuthInfo 5 { 6 String[] authId() default {"0001"}; 7 } 8 9 @Aspect 10 @Component 11 public class AuthService 12 { 13 @Around("within(com.company.product..*) && @annotation(authInfo)") 14 public Object aroundMethod(ProceedingJoinPoint joinPoint, AuthInfo authInfo) throws Throwable 15 { 16 String[] token = authInfo.authId(); 17 if (isValidate(token)) 18 { 19 return joinPoint.proceed(); 20 } 21 HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getResponse(); 22 response.setStatus(401); 23 return null; 24 } 25 26 private boolean isValidate(String[] tokenArray) 27 { 28 return true; 29 } 30 } 31 32 @RestController 33 public class TestController 34 { 35 @GetMapping("/test/{userId}") 36 @AuthInfo //通用校验 37 public String test(HttpServletRequest req, @PathVariable(value = "userId") String userId) 38 { 39 if(!isValidateUser(userId)){ //个别校验 40 throw new MyException("Illegal userId"); 41 } 42 ... ... 43 } 44 }
以上两种都属于AOP的应用,如果不希望Controller内包含了大量的if校验,可以考虑用上述两种方法抽出来。推荐使用Filter,自定义切面会造成额外的负担。
1.2 业务逻辑
输入校验完成后,到了真正处理业务逻辑的地方,推荐的做法是一行代码搞定。
1 @RestController 2 public class TestController 3 { 4 @Autowired 5 TestService testService; 6 7 @GetMapping("/test/{userId}") 8 @AuthInfo(authId = {"token"}) 9 public ResponseEntity test(HttpServletRequest req, @PathVariable(value = "userId") String userId) 10 { 11 if (!isValidateUser(userId)) 12 { 13 throw new MyException("Illegal userId"); 14 } 15 Object result = testService.getResult(userId); 16 return ResponseEntity.ok(result); 17 } 18 }
有人会问:我的实际业务逻辑中需要调用多个service怎么办?我的意见是,controller中不要涉及业务逻辑组装,组装的工作应该新建一个Service,在这个Service中完成。
1.3 控制输出
在上面的示例中已经涉及到了一些输出控制:自定义ResponseEntity和抛出异常。这两种方法可以灵活运用,自定义返回比较直接,可以很直接的返回status和消息体。
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(message);
而抛出异常则需要与ControllerAdvice相配合:在Controller中抛出的异常可被ControllerAdvice捕获,并根据异常的内容和种类,定制不同的返回。
1 @ControllerAdvice
2 public class MyExceptionHandler
3 {
4 private static Logger logger = LoggerFactory.getLogger(MyExceptionHandler.class);
5
6 @ExceptionHandler(MyException.class)
7 public ResponseEntity<ExceptionResponse> handleMyException(HttpServletRequest request, MyException ex)
8 {
9 String message = String.format("Request to %s failed, detail: %s", getUrl(request), ex.getMessage());
10 logger.error(message);
11 HttpStatus status = getHttpStatus(ex);
12 if (ExceptionCode.PARAM_CHECK_ERROR.equals(ex.getCode()))
13 {
14 status = HttpStatus.BAD_REQUEST;
15 }
16 return generateErrorResponse(status, getMessageDetail(ex));
17 }
18
19 @ExceptionHandler(JsonMappingException.class)
20 public ResponseEntity<ExceptionResponse> handleJsonMappingException(HttpServletRequest request, Exception ex)
21 {
22 String message = String.format("Parse response failed, url: %s, detail: %s", getUrl(request), ex.getMessage());
23 logger.error(message, ex);
24 return generateErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, getMessageDetail(ex));
25 }
26
27 @ExceptionHandler(Exception.class)
28 public ResponseEntity<ExceptionResponse> handleException(HttpServletRequest request, Exception ex)
29 {
30 String message = String.format("Request to %s failed, detail: %s", getUrl(request), ex.getMessage());
31 logger.error(message, ex);
32 return generateErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, getMessageDetail(ex));
33 }
34
35 private ResponseEntity<ExceptionResponse> generateErrorResponse(HttpStatus httpStatus, String message)
36 {
37 ExceptionResponse response = new ExceptionResponse();
38 response.setCode(String.valueOf(httpStatus.value()));
39 response.setMessage(message);
40 return ResponseEntity.status(httpStatus).body(response);
41 }
42 }
ControllerAdvice需要自定义异常MyException和自定义返回ExceptionResponse配合,定制自由度比较大,各微服务之间统一格式即可。
在SpringBoot应用里,ControllerAdvice是必备的,主要原因:
- RestTemplate大量使用,RestTemplate默认的ResponseErrorHandler中,非2XX的返回一律抛出异常;
- Service或其他组件中抛出的RuntimeException易被忽略;
- 异常返回统一在ControllerAdvice中定制,避免各个程序猿在各自的Controller中返回千奇百怪的Response。
2.Service
Service是真正的业务逻辑层,这一层的功能边界:
- 基于单一职责的原则,每一个Service只处理单一事务;
- 如果某个业务需要调用多个业务事务,建议在Service上再扩展一层,专门用于组装各个Service的调用;
- Service层不做任何形式的持久化工作:数据库访问、远程调用等。
3.Repository
微服务不赞同任何形式的状态如缓存,在多实例下,存在于各自JVM中的缓存由于互相不感知,可能会造成多实例之间的沟通问题。这就是为什么Eureka核心功能只是个RestTemplate的Inteceptor,缺花费了大力气做实例间的缓存同步的原因。
持久层Repository的功能是花样百出的持久化:
- 数据库访问
- 本地文件
- HTTP调用
- ... ...
可以看出,Repository层做的工作实际上是对网络上各种资源的访问。
4.Component
Controller、Service、Repository都是继承自Component,当你实在不好注解你的类但又希望Spring上下文去管理它时,可以暂时将其注解为Component。
个人认为出现这种尴尬问题的主要原因是因为类的功能不够单一,只要能够拆分重构,是可以确切的找到合适的注解的。
5.Resource
将Resource列举在此实际是不合适的,因为Resource是JDK的注解,但使用时确实易与其他几个注解造成混淆。
Resouce的使用场景时这样的:
你在微服务中将User信息持久化在MySQL中,并依此写了一个UserMySQLRepository去进行交互;
但是boss突然觉得MySQL一点也不好,希望你改成Redis的同时,保持对MySQL的支持以免有问题时能够回退。
这样你的微服务中就有了两个IUserRepository的实现类:UserMySQLRepository和UserRedisRepository。
在Service中如何调用它呢,如果还是使用以前的代码调用:
@Autowired
IUserRepository userRepo;
这样UserMySQLRepository和UserRedisRepository是要打架的:我也是IUserRepository,凭什么你上?
如果你这样调用:
@Autowired
UserMySQLRepository userMSRepo;
@Autowired
UserRedisRepository userRedisRepo;
代码的扩展性被破坏的一干二净:你的方法中必须用额外的代码去判断使用哪个repository;万一哪天boss觉得redis又不好了,难道再加一个Autowired?
这时候Resource可以闪亮登场了,最佳的实践如下:
@Repository("mysql") public class UserMySQLRepository implements IUserRepository {} @Repository("redis") public class UserRedisRepository implements IUserRepository {} @Service public class UserService implements IUserService { @Resource(name = "${user.persistence.type}") private IUserRepository userRepo; ... ... }
在application.properties中,可以添加一个配置去控制持久层到底使用MySQL还是Redis。
user.persistence.type=redis
#user.persistence.type=mysql
如果想切回MySQL,只要将user.persistence.type的值改回mysql即可。
至于Resource可以做到而Autowired做不到的原因,网上也有很多解释,做简单说明:
- Resource优先按照名称(注解中的value:mysql和redis)装配注入,也支持按照类型
- Autowired按照类型(class名)装配注入
这篇文章也是想到哪写到哪,不符合单一职责,有时间重构,里面的很多点可以单独成文。
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
- springboot2配置JavaMelody与springMVC配置JavaMelody 2020-06-11
- 蚂蚁金服这套SpringMvc面试题你懂多少(面试必刷) 2020-05-27
- SpringMVC高级-拦截器如何正确运用?案例详解 2020-05-21
- 萌新学习SpringMVC 2020-05-20
- SpringMVC中如何获取请求参数?案例详解 2020-05-19
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash