[Java]-服务端RestAPI接口封装简介


在《 HTTP客户端工具OkHttp简介》中介绍了RestAPI客户端请求方式;《 HTTP请求中RestAPI接口中时间序列化处理》中介绍了针对时间序列化处理。

RestAPI

RESTful是目前非常流行的一种互联网软件架构,其核心概念是资源(网络中任何东西都看作资源)。通过URL定位资源,用HTTP请求方式表示操作:

  • URL:要什么(资源)?
  • HTTP Method:干什么(动作)?
  • Status Code:结果怎样?

Content-Type

MediaType,即是Internet Media Type,互联网媒体类型;也叫做MIME类型,在Http协议消息头中,使用Content-Type来表示具体请求中的媒体类型信息。常见的媒体格式类型如下:

  • text/html : HTML格式

  • text/plain :纯文本格式

  • text/xml : XML格式

  • image/gif :gif图片格式

  • image/jpeg :jpg图片格式

  • image/png:png图片格式

  • application/xhtml+xml :XHTML格式

  • application/xml : XML数据格式

  • application/atom+xml :Atom XML聚合格式

  • application/json : JSON数据格式

  • application/pdf :pdf格式

  • application/msword : Word文档格式

  • application/octet-stream : 二进制流数据(如常见的文件下载)

  • application/x-www-form-urlencoded : form表单数据(会被编码为key/value格式发送到服务器)

  • multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式

Servlet

Servlet 是运行在 Web 服务器或应用服务器上的程序,它是’Web 浏览器或其他 HTTP 客户端的请求‘和‘ HTTP 服务器上’的数据库或应用程序之间的中间层。
Servlet流程

在接收到客户端请求时,Servlet容器会为请求创建代表HTTP请求的ServletRequest对象,和对此请求进行响应的ServletResponse对象,并将这两个对象传给service方法,由service方法来处理请求。service方法会根据请求类型来做转发,调度不同的方法来处理请求。

HttpServletRequest

HttpServletRequest中包含了客户端HTTP请求的所有信息,主要为三部分信息:请求行、请求头、请求正文。

请求行

请求行中包含:请求方法、资源名、路径、Http版本等信息。HttpServletRequest中实现了一揽子的方法,以便简单快捷的操作获取:

方法声明功能说明
String getMethod()获取请求方式:如GET、POST等
String getRequestURI()获取资源名称部分:即位于URL的主机与端口之后,参数部分之前部分
String getQueryString()获取参数部分:即URL中问号(?)以后的内容
String getProtocol()获取协议名和版本:如HTTP/1.0或HTTP/1.1等
String getContextPath()获取URL中属于WEB应用程序的路径(以/开始,不含末尾/),表示相对于WEB站点根目录的部分,若请求属于根目录,则返回空字符串
String getServletPath()获取Servlet的名称或所映射的目录
String getRemoteAddr()获取远端(客户端)的IP地址
String getRemoteHost()获取客户端的完整主机名;若无法解析出,则返回其IP地址
int getRemotePort()客户端连接的端口号
String getLocalAddr()获取WEB服务器上接收当前请求的IP地址
String getLocalName()获取WEB服务器上接收当前请求的主机名
int getLocalPort()获取WEB服务器上接收当前请求的端口号
String getServerName()获取当前请求所指向的主机名:即HTTP请求消息中Host头字段所对应的主机名部分
int getServerPort()获取当前请求所所连接的服务器端口号:即HTTP请求消息中Host头字段所对应的端口号部分
String getScheme()获取请求协议名:如http、https、ftp等
StringBuffer getRequestURL()获取客户端发起请求时的完整URL,包括协议、服务器名、端口号、资源路径等,但不包括后面查询参数部分

请求头

通过请求头可向服务器传递附加信息,如:可接受的数据类型、是否保持TCP连接等。Servlet中也提供了一系列操作方法:

方法声明功能说明
String getHeader(String name)获取指定头的值:若头中未包含name,则返回null;若包含多个name字段,则返回第一个对应的值
Enumeration getHeaders(String name)返回指定头字段的可枚举集合对象
Enumeration getHeaderNames()返回所有请求头字段名的可枚举集合对象
int getIntHeader(String name)返回指定头字段对应int值:若字段不存在,返回-1;若字段不是int类型的,抛出NumberFormatExcption
long getDateHeader(String name)返回指定头字段对应GMT时间戳(毫秒级)
String getContentType()获取Content-Type字段值
int getContentLength()获取Content-Length头字段的值
String getCharacterEncoding()获取消息实体部分的字符集编码
Cookie[] getCookies()获取所有客户端传递过来的Cookie对象,若没有返回null

请求参数

请求参数中包含用户提交的数据,是请求中最重要的部分。Servlet中也提供了一系列操作方法:

方法声明功能说明
String getParameter(String name)获取指定名称的参数值:若没有,返回null;若有参数名,但没有参数值,返回空字符串;若有多个,返回第一个
String[] getParameterValues(String name)返回指定参数对应的所有值
Enumeration getParameterNames()返回请求消息中所有参数名的可枚举对象
Map getParameterMap()返回参数的Map对象(键值对)

HttpServletResponse

HttpServletResponse用于完成Http的响应。

状态码

默认情况下会返回如下状态码:

  • 200:程序正常执行(完整处理了Http请求)时返回的错误码;
  • 404:url路径错误时;
  • 500:程序执行过程中直接抛出异常时,返回此错误码;

也可通过函数设定:

方法声明功能说明
void setStatus(int sc)设定响应状态码
void sendError(int sc)使用指定的状态码向客户端返回一个错误响应,并清空缓冲区
void sendError(int sc, Sting msg)设定状态码与错误信息,并清空缓冲区

SpringBoot中支持

Spring Boot全面支持开发RESTful程序,通过不同的注解来支持前端的请求。

Controller与Method

Spring中通过注解来定义控制器与API接口:

  • @Controller:响应页面,表示当前的类为控制器;
  • @RestController:是@ResponseBody和@Controller的结合,表明当前类是控制器且返回的是一组数据(ResponseBody)而非页面;
  • @RequestMapping:告诉控制器URL映射的映射方式,以下是@RequestMapping(method = RequestMethod.XXX)的简写
    • @GetMapping:对应查询,表明是一个查询URL映射;
    • @PostMapping:对应增加,表明是一个增加URL映射;
    • @PutMapping:对应更新,表明是一个更新URL映射;
    • @DeleteMapping:对应删除,表明是一个删除URL映射;
@RestController
@RequestMapping("test") // 路径
public class TestController {
    // ...
}

RequestMapping

RequestMapping 用于映射请求。用于类级别时,将特定请求(路径)映射此类控制器上;用于方法时,会进一步控制映射关系:

  • value: 指定请求的实际地址, 比如 /action/info;
  • method: 指定请求的method类型, GET、POST、PUT、DELETE等;
  • consumes: 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;
  • produces: 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回;
  • params: 指定request中必须包含某些参数值是,才让该方法处理;
  • headers: 指定request中必须包含某些指定的header值,才能让该方法处理请求;

其中,consumes, produces使用content-typ信息进行过滤信息;headers中可以使用content-type进行过滤和判断。

可通过value映射到多个路径(可通过../mult, ../one, ../two来访问):

@GetMapping(value = {"multi", "one", "two"})
public String multiMap(HttpServletRequest request) {
    return request.getRequestURI();
}

ExceptionHandler

通过ExceptionHandler可以统一处理异常,先定义统一的异常类:

public class WebTestRuntimeException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    
    private int code;
    private Exception inner;

    public WebTestRuntimeException(int errorCode, String errorMsg)     {
        super(errorMsg);
        this.code = errorCode;
    }
	// ...
}

以及统一的应答类:

public class BasicResponse implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private int               error;
    private String            errorMsg;
    private Object            result           = null;
	// ...
}

统一异常捕捉

定义如下类统一处理异常:

@Slf4j
@ControllerAdvice
public class ControllerExceptionHandler {
    @ResponseBody
    @ExceptionHandler
    public BasicResponse WebExceptionHandler(Exception ex) throws NoSuchFieldException {
        BindingResult bindingResult = null;
        if (ex instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException exArg = (MethodArgumentNotValidException) ex;
            bindingResult = exArg.getBindingResult();
        } else if (ex instanceof BindException) {
            bindingResult = ((BindException) ex).getBindingResult();
        }

        try {
            if (bindingResult != null && bindingResult.hasErrors()) {
                log.error("WebTest全局异常处理信息:{}", ex.getMessage());
                String fieldError = bindingResult.getFieldError().getDefaultMessage();
                return buildInvalidResult(fieldError);
            }

            if (ex instanceof ConstraintViolationException) {
                log.error("WebTest全局异常处理信息:{}", ex.getMessage());
                ConstraintViolationException e = (ConstraintViolationException) ex;
                String fieldError = e.getConstraintViolations().stream().findFirst().get().getMessage();
                return buildInvalidResult(fieldError);
            }
        } catch (Exception e) {
            log.error("process argumentsError throws error", e);
            return new BasicResponse(ErrorCodeEnum.ERROR_Invalid_Argument.getCode(), ex.getMessage());
        }

        if (ex instanceof WebTestRuntimeException) {
            log.error("WebTest全局异常处理信息:{}", ex.getMessage());
            WebTestRuntimeException hnException = (WebTestRuntimeException) ex;
            return new BasicResponse(hnException.getCode(), hnException.getMessage());
        }
        log.error("WebTest全局异常处理信息", ex);
        return new BasicResponse(ErrorCodeEnum.ERROR_FAIL.getCode(), ex.getMessage());
    }

    private BasicResponse buildInvalidResult(String field) {
        String[] fieldError = field.split("=",2);

        Integer strCode;
        String strMsg;
        if(fieldError.length>1){
            strCode =Integer.valueOf(fieldError[0].trim());
            strMsg = fieldError[1];
        }
        else{
            strCode = ErrorCodeEnum.ERROR_Invalid_Argument.getCode();
            strMsg = fieldError[0];
        }
        return new BasicResponse(strCode, strMsg.trim());
    }
}

buildInvalidResult是根据参数上@Validated验证失败(抛出ConstraintViolationException等异常)构造错误异常返回。

参数自动验证

Spring验证框架提供了入参检验注解:
Spring参数验证注解

为了使用Validated自动验证,定义时需要增加注解(@Pattern注解允许对应变量为空,只有非空时才做验证):

@Data
@ApiModel
public class AddUserRequest implements Serializable {
    private static final long serialVersionUID = 1L;

	@NotBlank(message = "100001= idName不能为空")
    private String idName;

    @Pattern(regexp = "[0-3]", message = "100001= type错误,只能(0-3)的数字")
    private String type; // 可以为空;非空只能是0~3的数字

	@Pattern(regexp = "|\\d{7,11}", message = "100001= phone格式不正确(11位数字)")
    private String phone; // 可以为空;非空只只能是7~11位数字

	@NotNull(message = "100001= isSelf不能为空")
	@Range(min = 0, max = 10, message = "100001= count必须是0~10")
    private Integer count; // Range注解不能处理null,需要增加NotNull限制
}

为了使前面的注解生效,需要在参数上添加@Validated注解:

@PostMapping(value = "addUser")
public BasicResponse addUser(@Validated @RequestBody AddUserRequest reqBody) {
    // ...
}

当传入的参数验证失败时,会抛出ConstraintViolationException异常,然后就会由前面的ControllerExceptionHandler捕捉。

API接口示例

RestAPI接口统一放在Controller类中:

@RestController
@RequestMapping("test")
public class TestController {
    @GetMapping("paramGet")  // 请求 ../test/paramGet?msg=12
    public String paramGet(HttpServletRequest request, HttpServletResponse resp, String msg) throws IOException {
        resp.setStatus(2XX); // 设定返回状态码,默认200
        return msg;
    }

    @PutMapping(value = "paramPut") // 请求 ../test/paramPut?msg=12&others=34
    public String paramPut(@RequestParam String msg, @RequestParam String others) {
		// ...
    }

    @PutMapping("testPut") // data以body方式传递
    public void testPut(@RequestBody TestData data) {
		// ...
    }

    @DeleteMapping(value = "testDelete") // 请求 ../test/testDelete?id=12
    public String testDelete(@RequestParam String id) {
		// ...
    }

    @PostMapping("testPost")
    public void testPost(@RequestBody TestData data) {
		// 自动解析为类(需要Json格式匹配)
    }

    @PostMapping(value = "postMsg", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public String postMsg(@RequestBody String msg) {
		// 所有body以字符串方式获取,然后自己解析 
    }
        
    @GetMapping("/msg/{key}") // 请求 ../test/12
    public String pathMsg(@PathVariable("key") String key) {
        // 从路径上获取值,key为12
    }
}

Form方式请求

form是一种常用的请求方式,其参数是作为body方式传递(POST中body默认是Json字符串;GET中参数是放在URL上):

//  curl -X POST ".../test/formMsg" -H "accept: */*" -H "Content-Type: application/x-www-form-urlencoded" -d "msg=123&others=234"
@PostMapping(value = "formMsg", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String formMsg(@RequestParam String msg, @RequestParam String others) {
	// ...
}

文件上传

通过MultipartFile可以方便地上传文件:

@PostMapping(value = "/uploadMulti", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String uploadMulti(@RequestParam("file") MultipartFile[] files) {
    for (MultipartFile partFile : files) {
        _logger.info("File: {}, Ori: {}, Type: {}, Size: {}",
                partFile.getName(), partFile.getOriginalFilename(),
                partFile.getContentType(), partFile.getSize());

        StringBuilder sb = new StringBuilder("\n");
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(partFile.getInputStream()))) {
            String line = reader.readLine();
            while (line != null) {
                sb.append(line);
                sb.append("\n");

                line = reader.readLine();
            }

            _logger.info("Contents: {}", sb.toString());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    return "Files: " + Arrays.stream(files).map(MultipartFile::getOriginalFilename).collect(Collectors.joining("; "));
}

// curl -X POST ".../test/uploadFile" -H "accept: */*" -H "Content-Type: multipart/form-data" -F "file=@Python-learning.md;type="
@PostMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String uploadFile(@RequestParam("file") MultipartFile file) {
    _logger.info("File: {}, Ori: {}, Type: {}, Size: {}",
            file.getName(), file.getOriginalFilename(),
            file.getContentType(), file.getSize());

    StringBuilder sb = new StringBuilder("\n");
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
        String line = reader.readLine();
        while (line != null) {
            sb.append(line);
            sb.append("\n");

            line = reader.readLine();
        }

        _logger.info("Contents: {}", sb.toString());
    } catch (Exception ex) {
        ex.printStackTrace();
    }

    return String.format("received: %s, size: %d, type: %s", file.getOriginalFilename(),
            file.getSize(), file.getContentType());
}

swagger中无法处理上传多个文件,通过OkHttp可以方便地实现:
在这里插入图片描述

(1)项目简介 这个demo很简单,是一个记账小工程。用户可以注册、修改密码,可以记账、查找记账记录等。 (2)接口介绍 用户操作相关: post /users 用户注册 post /users/login 用户登录(这里我把login当成一个名词) put /users/pwd?userId=xxx&sign=xxx 用户修改密码 delete /users?uerId=xxx&sign=xxx 删除用户 记账记录操作相关: post /records?userId=xxx&sign=xxx 增加一条记账记录 get /records/:id?userId=xxx&sign=xxx 查询一条记账记录详情 put /records/:id?userId=xxx&sign=xxx 修改一条记账记录详情 get /records?查询参数&userId=xxx&sign=xxx 分页查询记账记录 delete /records/:id?userId=xxx&sign=xxx 删除一条记账记录 其中url中带sign参数的表示该接口需要鉴权,sign必须是url中最后一个参数。具体的鉴权方法是:用户登录后,服务器生成返回一个token,然后客户端要注意保存这个token,需要鉴权的接口加上sign签名,sign=MD5(url+token),这可以避免直接传token从而泄露了token。这里我觉得接口最好还带一个时间戳参数timestamp,然后可以在服务端比较时间差,从而避免重放攻击。而且这还有一个好处,就是如果有人截获了我们的请求,他想伪造我们的请求则不得不改时间戳参数(因为我们在服务器端会比较时间),这一来sign势必会改变,他是无法得知这个sign的。如果我们没有加时间戳参数的话,那么,他截获了请求url,再重发这个请求势必又是一次合法的请求。我在这里为了简单一些,就不加时间戳了,因为这在开发测试阶段实在是太麻烦了。 (3)关于redis和数据库的说明 服务端在用户登录后,生成token,并将token保存到redis中。后面在接口鉴权的时候会取出token计算签名MD5(除sign外的url+token),进行比对。 这个demo搭建了一个redis主从复制,具体可以参考:http://download.csdn.net/detail/zhutulang/9585010 数据库使用mysql,脚本在 src/main/resources/accounting.sql
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值