第十章 后端MVC服务

1 依赖注入

@Autowired

在Controller层引用Service层的类时,或者在Service层引用Dao层的类时,我们不能仅仅用一般的声明变量的方法来创建一个对象,取而代之的是利用@Autowired注解进行依赖注入,即将原先声明变量时声明的权限范围(public/protected/private)替换为@Autowired。之后我们就可以在函数实现中引用该对象的内部函数。

2 业务逻辑层(service)

严格来说,系统的复杂业务逻辑,都应该集中在业务逻辑层中处理。由于业务逻辑层位于接口层(Controller)与数据访问层(dao)之间,实现了对这两层的解耦。这样,如果前端数据表现形式发生变化,不影响数据库访问层的代码实现,而如果数据库管理系统发生变化,同样也不需要修改前端接口。本例中由于业务逻辑非常简单,所以业务逻辑层(service)只是起到接口层(Controller)与数据访问层(dao)之间的桥梁作用。

3 接口层(Controller)

首先要介绍的是接口的类型。常用的接口类型有五种:POST、DELETE、PUT、PATCH和GET,分别对应增删改查的业务逻辑。虽然它们代表的逻辑功能不同,但从书写与实现上来说,它们并没有太大的差别(GET方法无法传递RequestBody形式的参数,而其他三种可以),你同样可以用POST来实现修改,或是用PUT来实现删除,但这样是不规范的,会使代码可读性变低。

接下来具体讲述接口及接口类的实现。从本质上来说,每个接口都是一个接口类中的函数,加上一些注解来实现的。因此我们将以一个接口的书写顺序重点讲解 @RestController、@RequestMapping、@ResponseStatus、@ResponseBody、@RequestBody、@RequestParam、@PathVariable 和 @Autowired 等几个注解。

@RestController

@RestController用于标明该类在该项目中扮演的角色,告知编译器这是一个RESTFul接口类。我们需要在每个类声明之前添加@RestController。后面我们在介绍dao层与service层时会介绍 @Repository 注解与 @Service 注解,作用与 @RestController 类似,都是标明角色的。

@RequestMapping

每个接口都有一个地址,以供前端能够访问到,这个地址正如字面意思,最终表示为一个url,而@RequestMapping就是用于为接口分配这样一个接口,因此这个注解一般会添加在每个接口函数的声明之前,它的可选参数有六个:

value与method为常用的基础参数,前者用于声明地址,后者用于声明接口的类型,例如:@RequestMapping(value="/new", method = RequestMethod.GET)就声明该接口url为:你的服务器地址/new,类型为get。需要注意的是,value中可以传递参数,具体方法我们会在之后的@@PathVariable中讲到,在这里我们先不讨论它。此外,value可以有多个值,表明该接口对应多个url,书写格式为value= {"url1","url2"}.

该注解的另外四个可选参数为:consumes、produces、params、headers。它们用于筛选接口所收到的请求,可以大大减小服务器压力。我们看一个例子:

@RequestMapping(value="/new", 
    method =RequestMethod.GET,
    consumes="application/json",
    produces="application/json",
    params="myParam=myValue",
    headers="Referer=http://www.abc.com/")

在该例中,consumes="application/json"说明了该接口只处理Content-Type为“application/json”的请求。produces="application/json"说明该接口只处理Accept头中包含了"application/json"的请求,同时暗示了返回的内容类型为application/json。params="myParam=myValue"表明该接口只处理参数中包含名为myParam,且该参数值为myValue的请求。而最后的 headers="Referer=http://www.abc.com/" 则说明该接口仅处理请求的头部中包含了指定“Refer”请求头和对应值为“http://www.abc.com/”的请求。

当然,对于初学者来说,我们只需要记住并学会value与method的用法即可。

还值得一提的是,虽然 @RequestMapping 注解用于标明接口的访问地址,但我们通常也在接口类之前添加该注解。因为我们总是把同一个功能模块的接口放到一个接口类中,而接口类之前的 @RequestMapping 就是用于声明代表接口所属功能模块的url,当然,这里的 @RequestMapping 注解只有,也只能有value一个参数。而它带来的接口访问方式的变化,举例来说,之前我们访问@RequestMapping(value="/new", method = RequestMethod.GET)声明的接口时,url为:你的服务器地址/new,现在,我们在接口类之前添加@RequestMapping("/classMapping"),我们访问时的url就变成了:你的服务器地址/classMapping/new,仅此而已。

@ResponseStatus

该注解同样需要在接口声明之前添加,它用于给Http回应(Response)添加一个状态,用于说明这次访问接口的结果。对于一次成功的访问,一般我们只需回复一个告知成功的状态信息。例如@ResponseStatus(HttpStatus.OK),或@ResponseStatus(HttpStatus.CREATED)。当然,这个注解的主要功能并不止于此。它更多地用于异常处理。但是,异常处理较为复杂,也许初学者想要一次性掌握较为困难,因此以下关于异常处理的讲解若你觉得难以理解,请略过,它并不是我们讲述的重点。

若你有一些JAVA编程经验,就应该知道,我们偶尔会在实现一些功能时,抛出一些异常,为了处理这些异常。我们会抛出一些自己定义的异常,然后添加一些处理方法。那么,在实现接口时,若出现了异常,怎么告知前端呢?我们用到的就是@ResponseStatus注解。例如,我们在某个接口中抛出了UserNotExistException异常,这个异常类需要我们自己创建,那么在创建该类时,我们就可以在类声明前添加@ResponseStatus,这个注解有两个属性:一个是我们之前提到的状态码,如HttpStatus.FORBIDDEN等,另一个为reason属性,使我们可以具体说明此次异常的原因,如reason=”用户不存在”。这两个属性会被封装在返回给前端的回应中,便于前端调试。

@RequestParam、@RequestBody、@PathVariable

这三个注解用于从前端提交的请求中获取参数,它们都是添加在接口函数的参数之前。

*@RequestPara传递的参数以键值对的形式封装在请求中,我们常见到的url网址中在?后添加的参数即对应该注解形式,一般的基本类型的参数(如int,String,float等)都可以通过这种方式传递。它有两个可选参数,第一个为value,即该变量在请求中的名称,若不添加该参数,则默认为接口函数中声明的参数变量名。第二个参数为required,注明该变量是否为访问该接口的请求所必须携带的,默认为true,即必须携带。需要注意的是,若某个参数的required为false,则参数会被默认赋值为null,因此某些在java中不能被赋值为null的数据类型(如int)的参数是不能设置其required为false 的。

@RequestBody所注明的参数会被适当的HttpMessageConverter根据请求的content-Type转换为适当的形式。因此,若传入参数是我们在model层定义的实体类,或者参数不为普通的键值对时,我们可以,也只能选用这种方式来传入参数。因此与@RequestParam不同,它注明的参数也无法通过url直接传入。例如:@RequestBody Person p,其中Person为我们在后端自定义的实体类,前端显然无法通过键值对来传递一个Person给后端,因为Person在前端并不存在,只能通过一个json格式的数据来传递,如:{“p”:{“name”:”Tom”,”age”:18}}.此外,正如在介绍接口类型时所说,GET类型的接口是无法接收@RequestBody格式的参数的,因为从逻辑上来说,我们一般只有在添加或修改时才需要传递整个实体,而根据某个属性去查询或删除(因此虽然DELETE类型接口中能够接收实体型参数,我们一般不这么做)。

@PathVariable正如字面意思所说,它用于标注那些存在于路径,即url中的变量。如我们在介绍@RequestMapping时所说,url中可以携带参数,参数在其中的存在形式为:{参数名},如:

@RequestMapping(value = "/heroes/{id}", method = RequestMethod.GET)
@ResponseBody
public Hero getHero(@PathVariable long id) {
}

在上例中,我们为参数id添加了@PathVariable,那么url为/heroes/1,2,3,4,5......的get类型的访问请求都会找到该接口,而spring会从具体每个请求的url中找到{id}位置对应的值,并将它赋值给函数的参数:id。需要注意,url中的变量名与函数的参数名应尽量保持一致。此外,我们也可以通过正则表达式为url中的参数设定格式,如{id:\d.\d.\d}但这种用法并不常见,在这里我们不多做介绍。

@ResponseBody

之前我们提到过,@RequestBody是用于封装请求。而@ResponseBody与它类似,只不过它往往是用于将接口的返回值封装至响应,它可以添加在类声明之前,也可以添加在返回值类型声明之前。一般只要接口返回了数据(即接口函数返回值类型不为void),我们都需要添加该注解。

以上,书写Restful所主要用到的几个注解全部讲解完毕。接口层完成实现。

4 身份认证

World Wide Web是一个无状态的网络,HTTP协议并不维护连接的状态。但是,Web应用需要后端与前端维护身份认证机制。常用的身份认证方法有:

  • 基于会话(session)
  • 基于token(例如:JWT)
  • 基于第三方认证(例如:OAuth2)

如果考虑前端的多样性,例如支持手机的Native APP,会话并不是一个很好的方案。第三方认证又比较复杂。所以本文示例采用基于JWT的token认证机制。简单来说,认证过程如下:

  1. 向后端/auth发送一个POST请求,Request Body为JSON格式,形式为:

    {
     "username":"",
     "password":""
    }
    

    request headers 应包含:Content-Type:Application/json

  2. 如果用户名和密码验证成功,后端会反馈一个JWT格式的token。

  3. 从此之后,访问后端受保护的资源时,应该在 request headers 处增加:Authorization,值为Bearer+token,就可以完成身份认证,访问受保护的资源。

需要说明的是,在生产环境下,为了安全可靠的数据传输,前后端通信应该始终使用HTTPS加密协议传输数据。

5 总结

现在,后端服务实现完成,我们可以将前端访问后端的接口改为访问 spring boot 项目提供的 RESTful API。缺省的服务端口是8080。

至此,整个“英雄指南”示例项目设计和实现完毕。

点此处可以下载本章完成后的示例代码。 前端示例代码 后端示例代码 数据库示例代码

results matching ""

    No results matching ""