欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

SpringMVC体系下各组件的功能边界及重构建议

程序员文章站 2023-09-28 15:32:27
最近在重构后端代码,很多同学对Spring体系下的后端组件如Controller、Service、Repository、Component等认识不够清晰,导致代码里常常会出现Controller里直接使用RestTemplate、直接访问数据库的情况。下面谈谈我对这些组件功能边界的认识,一家之言,欢 ......

最近在重构后端代码,很多同学对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名)装配注入

这篇文章也是想到哪写到哪,不符合单一职责,有时间重构,里面的很多点可以单独成文。