011 RESTful接口优化

来源:互联网 发布:博彦多彩数据招聘 编辑:程序博客网 时间:2024/06/14 12:51

011 RESTful接口优化

视频分享到youtube上了。
https://youtu.be/2oNGCB_j6V0
优酷链接
http://v.youku.com/v_show/id_XMjgzOTg0MzUzNg==.html?f=49760672

以下RESTful API 设计参考,摘录至网络:下面是链接
http://www.ruanyifeng.com/blog/2014/05/restful_api.html
RESTful API 设计参考:

  • 协议:使用Https协议
  • 域名:应该尽量将API部署在专用域名之下。
    https://api.example.com
  • 版本:应该将API的版本号放入URL
    https://api.example.com/v1/
  • 路径(Endpoint):表示API的具体网址
    在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的”集合”(collection),所以API中的名词也应该使用复数。
https://api.example.com/v1/zooshttps://api.example.com/v1/animalshttps://api.example.com/v1/employees
  • HTTP动词:
GET(SELECT):从服务器取出资源(一项或多项)。POST(CREATE):在服务器新建一个资源。PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。DELETEDELETE):从服务器删除资源。

还有两个不常用的HTTP动词。

HEAD:获取资源的元数据。OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

下面是一些例子。

GET /zoos:列出所有动物园POST /zoos:新建一个动物园GET /zoos/ID:获取某个指定动物园的信息PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)DELETE /zoos/ID:删除某个动物园GET /zoos/ID/animals:列出某个指定动物园的所有动物DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物
  • 过滤信息(Filtering)
    如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。下面是一些常见的参数。
?limit=10:指定返回记录的数量?offset=10:指定返回记录的开始位置。?page=2&per_page=100:指定第几页,以及每页的记录数。?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。?animal_type_id=1:指定筛选条件

参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。

  • 状态码(Status Codes)
    服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)204 NO CONTENT - [DELETE]:用户删除数据成功。400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

状态码的完全列表:
https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

  • 错误处理(Error handling)
    如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。
{    error: "Invalid API key"}
  • 返回结果

针对不同操作,服务器向用户返回的结果应该符合以下规范。

GET /collection:返回资源对象的列表(数组)GET /collection/resource:返回单个资源对象POST /collection:返回新生成的资源对象PUT /collection/resource:返回完整的资源对象PATCH /collection/resource:返回完整的资源对象DELETE /collection/resource:返回一个空文档
  • 其他

API的身份认证应该使用OAuth 2.0框架。
服务器返回的数据格式,应该尽量使用JSON,避免使用XML

为了解决某个添加用户的接口,由于业务导致字段属性变化很大,只能重新定义一个新的接口,由 /v1/users 变成了 /v2/users,这样我们就要维护两套接口的逻辑。同时如果使用更高的版本访问例如:v4/users访问,还可以自动适配到最新的v2版本。这里的自动适配要打个问号?这么做可能会产生不期望的后果。
参考自下面的链接:http://www.cnblogs.com/jcli/p/springmvc_restful_version.html

创建utils包,添加ApiVersion类

import java.lang.annotation.ElementType;import java.lang.annotation.RetentionPolicy;/** *  * 接口版本标识注解 * */@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Mappingpublic @interface ApiVersion {    int value();}

添加ApiVesrsionCondition类

public class ApiVesrsionCondition implements RequestCondition<ApiVesrsionCondition> {    // 路径中版本的前缀, 这里用 /v[1-9]/的形式    private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/");    private int apiVersion;    public ApiVesrsionCondition(int apiVersion){        this.apiVersion = apiVersion;    }    public ApiVesrsionCondition combine(ApiVesrsionCondition other) {        // 采用最后定义优先原则,则方法上的定义覆盖类上面的定义        return new ApiVesrsionCondition(other.getApiVersion());    }    public ApiVesrsionCondition getMatchingCondition(HttpServletRequest request) {        String requestUrl = request.getRequestURL().toString();//得到请求的URL地址         String requestUri = request.getRequestURI();//得到请求的资源        Matcher m = VERSION_PREFIX_PATTERN.matcher(requestUri);        if(m.find()){            Integer version = Integer.valueOf(m.group(1));            if(version >= this.apiVersion) // 如果请求的版本号大于配置版本号, 则满足                return this;        }        return null;    }    public int compareTo(ApiVesrsionCondition other, HttpServletRequest request) {        // 优先匹配最新的版本号        return other.getApiVersion() - this.apiVersion;    }    public int getApiVersion() {        return apiVersion;    }}

添加CustomRequestMappingHandlerMapping类

@RequestMappingpublic class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {    @Override    protected RequestCondition<ApiVesrsionCondition> getCustomTypeCondition(Class<?> handlerType) {        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);        return createCondition(apiVersion);    }    @Override    protected RequestCondition<ApiVesrsionCondition> getCustomMethodCondition(Method method) {        ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);        return createCondition(apiVersion);    }    private RequestCondition<ApiVesrsionCondition> createCondition(ApiVersion apiVersion) {        return apiVersion == null ? null : new ApiVesrsionCondition(apiVersion.value());    }}

为了实现这个功能,需要使用java Config配置方式自定义RequestMappingHandlerMapping

这里有个参考迁移到Java Based Configuration的链接
https://www.luckyryan.com/2013/02/07/migrate-spring-mvc-servlet-xml-to-java-config/

使用 JavaConfig 的好处,Spring 官方文档的描述:

  • JavaConfig 为依赖注入提供了一个真正面向对象的机制,这意味着开发者可以在配置代码中充分利用 Java 重用、继承和多态性等特性。
  • 开发者可以完全控制实例化和依赖注入,即使是最复杂的对象也可以很优雅地处理。
  • 因为使用 JavaConfig 只需要 Java,可以很容易的 refactor 代码,而无需再 IDE 之外使用特殊的工具或插件。

创建config包,在config包下面创建WebMvcConfig类,实现自定义RequestMappingHandlerMapping的功能,因为需要注释掉springmvc.xml里面的mvc:annotation-driven所以在这里配置消息转换处理fastjson

@Configuration@EnableWebMvcpublic class WebMvcConfig extends WebMvcConfigurerAdapter{    @Autowired    private DateTimeConverter dateTimeConverter;    public void addFormatters(FormatterRegistry registry)    {        registry.addConverter(dateTimeConverter);    }    // equivalents for <mvc:resources/> tags    @Override    public void addResourceHandlers(final ResourceHandlerRegistry registry)    {        CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS);        registry.addResourceHandler("/resources/**").addResourceLocations("/resources/commons/")                .setCachePeriod(31556926).addResourceLocations("/resources/themeDefault/").setCachePeriod(31556926)                .setCacheControl(cc);        registry.setOrder(0);    }    @Bean    public StringHttpMessageConverter getStringHttpMessageConverter()    {        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(Charset.forName("UTF-8"));        List<MediaType> list = new ArrayList<MediaType>();        list.add(new MediaType("text", "plain", Charset.forName("UTF-8")));        list.add(new MediaType("*", "*", Charset.forName("UTF-8")));        stringConverter.setSupportedMediaTypes(list);        return stringConverter;    }    @Bean    public RequestMappingHandlerMapping requestMappingHandlerMapping()    {        RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();        handlerMapping.setOrder(0);        // handlerMapping.setInterceptors(getInterceptors());        return handlerMapping;    }    @Bean    public FastJsonHttpMessageConverter getFastJson()    {        FastJsonHttpMessageConverter jsonConverter = new FastJsonHttpMessageConverter();        List<MediaType> jsonList = new ArrayList<MediaType>();        jsonList.add(MediaType.valueOf("application/json;charset=UTF-8"));        jsonList.add(MediaType.valueOf("text/plain;charset=utf-8"));        jsonList.add(MediaType.valueOf("text/html;charset=utf-8"));        jsonConverter.setSupportedMediaTypes(jsonList);        FastJsonConfig fastJsonConfig = new FastJsonConfig();        fastJsonConfig.setCharset(Charset.forName("UTF-8"));        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat, SerializerFeature.WriteMapNullValue);        ValueFilter valueFilter = new ValueFilter()        {            public Object process(Object classRef, String key, Object value)            {                if (null == value)                {                    value = "";                }                return value;            }        };        fastJsonConfig.setSerializeFilters(valueFilter);        jsonConverter.setFastJsonConfig(fastJsonConfig);        return jsonConverter;    }    public void configureMessageConverters(List<HttpMessageConverter<?>> converters)    {        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(Charset.forName("UTF-8"));        List<MediaType> list = new ArrayList<MediaType>();        list.add(new MediaType("text", "plain", Charset.forName("UTF-8")));        list.add(new MediaType("*", "*", Charset.forName("UTF-8")));        stringConverter.setSupportedMediaTypes(list);        FastJsonHttpMessageConverter4 jsonConverter = new FastJsonHttpMessageConverter4();        List<MediaType> jsonList = new ArrayList<MediaType>();        jsonList.add(MediaType.valueOf("application/json;charset=UTF-8"));        jsonList.add(MediaType.valueOf("text/plain;charset=utf-8"));        jsonList.add(MediaType.valueOf("text/html;charset=utf-8"));        jsonConverter.setSupportedMediaTypes(jsonList);        FastJsonConfig fastJsonConfig = new FastJsonConfig();        fastJsonConfig.setCharset(Charset.forName("UTF-8"));        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);        jsonConverter.setFastJsonConfig(fastJsonConfig);        converters.add(stringConverter);        converters.add(jsonConverter);    }}

springmvc.xml还需要做修改,把config包加进去

<!-- 配置自动扫描的包 --><context:component-scan    base-package="com.study.controller,com.study.service,com.study.converters,com.study.config"></context:component-scan>

同时还要注释掉,包括上面的bean声明,这里的fastjson使用Java config方式添加配置,也可以注释掉了,在java config里面配置了。

<!-- <mvc:annotation-driven conversion-service="conversionService">    <mvc:message-converters register-defaults="true">        <ref bean="stringHttpMessageConverter" />        <ref bean="fastJsonHttpMessageConverter" />    </mvc:message-converters></mvc:annotation-driven> -->

在controller的路径映射里面添加版本信息v1,参考如下,同时修改对应的测试类。增加新的版本后,可以在方法里面覆盖类上面的版本。这个挺有用的,不用一个一个的写了。

@RequestMapping("/{version}/")@ApiVersion(1)@Controllerpublic class UserController{...    @ApiVersion(3)    @RequestMapping("/getRequest/{id}")    public String getRequestProc3(@PathVariable("id") Integer id,HttpServletRequest request, HttpServletResponse response)    {        System.out.println("getRequestProc3: " + id);        return "success";    }    @ApiVersion(5)    @RequestMapping("getRequest/{id}")    public String getRequestProc5(@PathVariable("id") Integer id)    {        System.out.println("getRequestProc5: " + id);        return "success";    }    ...

修改TeacherController增加版本信息

修改users.jsp,主要是静态资源的相对路径,和url的版本信息

<%@ page language="java" contentType="text/html; charset=UTF-8"    pageEncoding="UTF-8"%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>Insert title here</title><!-- 下面两个必须引用 --><script type="text/javascript"    src="../resources/jsExt/jquery-easyui-1.5.2/jquery.min.js"></script><script type="text/javascript"    src="../resources//jsExt/jquery-easyui-1.5.2/jquery.easyui.min.js"></script><!-- 国际化文件 --><script type="text/javascript"    src="../resources//jsExt/jquery-easyui-1.5.2/locale/easyui-lang-zh_CN.js"></script><!-- 默认样式 --><link rel="stylesheet"    href="../resources/jsExt/jquery-easyui-1.5.2/themes/default/easyui.css"><!-- 图标 --><link rel="stylesheet"    href="../resources/jsExt/jquery-easyui-1.5.2/themes/icon.css"><!-- 自定义js --><script type="text/javascript" src="../resources/js/teacherFormatter.js"></script><!-- 自定义的js脚本 --><script type="text/javascript" src="../resources/js/commons.js"></script><script type="text/javascript" src="../resources/js/user/userOper.js"></script><link rel="stylesheet" href="../resources/css/user.css"></head><body>    <div id="tt" class="easyui-layout" style="margin: 20px 20px 20px 20px;">        <table id="dg" class="easyui-datagrid" title="用户列表"            style="width: 100%; height: 400px;pagination-num{width:200px}"            data-options="rownumbers:true,striped:true,fitColumns:true,singleSelect:true,autoRowHeight:true,pagination:true,                pageSize:12,pageList:[12,100,200,300],url:'${pageContext.request.contextPath}/v1/getUsers',method:'get',toolbar:'#toolbar'">            <thead>                <tr>                    <th data-options="field:'id',width:100">ID</th>                    <th data-options="field:'name',width:100">name</th>                    <th data-options="field:'age',width:100">Age</th>                    <th data-options="field:'gender',width:60">Gender</th>                    <th data-options="field:'email',width:150">Email</th>                    <th data-options="field:'createTime',width:150">Create Time</th>                    <th data-options="field:'loginTime',width:150">Login Time</th>                    <th data-options="field:'teacher',width:100"                        formatter="teacherFormatter">Teacher Name</th>                </tr>            </thead>        </table>        <div id="toolbar">            <a href="#" class="easyui-linkbutton"                data-options="iconCls:'icon-add'" onclick="insert()">添加</a> <a                href="#" class="easyui-linkbutton"                data-options="iconCls:'icon-edit'" onclick="edit()">编辑</a> <a                href="#" class="easyui-linkbutton"                data-options="iconCls:'icon-remove'" onclick="del()">删除</a>        </div>    </div>    <!-- create user dialog -->    <div id="dlg" class="easyui-dialog"        data-options="iconCls:'icon-save',resizable:true,modal:true"        style="width: 400px; height: 280px; padding: 10px 20px" closed="true"        buttons="#dlg-buttons">        <div class="ftitle">用户信息</div>        <form id="fm" method="post">            <div class="fitem">                <label for="name">用户名:</label> <input name="name"                    class="easyui-validatebox" type="text" data-options="required:true">            </div>            <div class="fitem">                <label for="age">年 龄:</label> <input name="age"                    class="easyui-numberbox" type="text"                    data-options="required:true,validType:'number'">            </div>            <div class="fitem">                <label for="gender">性 别:</label> <input id="state1" name="gender"                    value="男" type="radio" checked="true" /><input id="state2"                    name="gender" value="女" type="radio" /></div>            <div class="fitem">                <label for="email">Email:</label> <input name="email"                    class="easyui-validatebox" type="text"                    data-options="required:true,validType:'email'">            </div>            <div class="fitem">                <label for="teacherId">教 师:</label> <input id="cc"                    class="easyui-combobox" name="teacherId"                    data-options="valueField:'id',textField:'name',panelHeight:80,editable:false,method:'get',url:'${pageContext.request.contextPath}/v1/getTeacherComboData'">            </div>        </form>    </div>    <div id="dlg-buttons">        <a href="#" class="easyui-linkbutton" data-options="iconCls:'icon-ok'"            onclick="save()">Save</a> <a href="#" class="easyui-linkbutton"            data-options="iconCls:'icon-cancel'"            onclick="javascript:$('#dlg').dialog('close')">Cancel</a>    </div></body></html>

修改userOper.js增加版本信息

/*! * imalex@163.com - v0.1 (2015-08-29 18:00:00 +0800) * Copyright 2015 */$(function() {});$('#dg').pagination({    pageSize: 20,//每页显示的记录条数,默认为10     pageList: [20, 50, 100],//可以设置每页记录条数的列表 });function insert() {    $('#dlg').dialog('open').dialog('setTitle', '新建用户');    $('#fm').form('clear');    url = ctx + '/v1/users';    type = insert;}function edit() {    var row = $('#dg').datagrid('getSelected');    if (row) {        $('#dlg').dialog('open').dialog('setTitle', '编辑用户信息');        $('#fm').form('load', row);        //setCheckedValue(document.forms['fm'].elements['gender'], row['gender']);        url = ctx + '/v1/users/' + row.id;        type = edit;    }}function save() {    $('#fm').form('submit', {        url : url,        type:type,        onSubmit : function(param) {            if (type == edit) {                //use put                 param._method ='PUT';            }            param.createTime = new Date().Format("yyyy-MM-dd hh:mm:ss");            param.loginTime = param.createTime;            var ret = $(this).form('validate');            return ret;        },        success : function(result) {            var result = eval('(' + result + ')');            if (result.errorMsg) {                $.messager.show({                    title : 'Error',                    msg : result.errorMsg                });            } else {                $('#dlg').dialog('close'); // close the dialog                $('#dg').datagrid('reload'); // reload the user data            }        }    });}function del() {    var row = $('#dg').datagrid('getSelected');    if (row) {        $.messager.confirm('确认', '请确认删除已选择用户?', function(r) {            if (r) {                $.post(ctx + '/v1/users/' + row.id, {                    _method : 'DELETE'                }, function(result) {                    if (result.status != null) {                        $('#dg').datagrid('reload'); // reload the user data                    } else {                        $.messager.show({ // show error message                            title : 'Error',                            msg : result.errorMsg                        });                    }                }, 'json');            }        });    }}

测试类补齐TestUserController里面的路径测试

@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:springmvc.xml")@WebAppConfiguration@FixMethodOrder(MethodSorters.NAME_ASCENDING)public class TestUserController{    public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(),            MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));    @Autowired    private WebApplicationContext wac;    private MockMvc mockMvc;    private static String lastNewId;    @Before    public void init()    {        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();    }    @Test    public void testReturnPage() throws Exception    {        mockMvc.perform(get("/v1/users")).andExpect(status().isOk()).andExpect(view().name("users")).andDo(print());    }    @Test    public void testReturnPageGet() throws Exception    {        mockMvc.perform(get("/v1/getRequest/1")).andExpect(status().isOk()).andExpect(view().name("success"))                .andDo(print());    }    @Test    public void testReturnPagePost() throws Exception    {        mockMvc.perform(post("/v1/postRequest")).andExpect(status().isOk()).andExpect(view().name("success"))                .andDo(print());    }    @Test    public void testReturnPageDelete() throws Exception    {        mockMvc.perform(delete("/v1/deleteRequest/1")).andExpect(status().isOk()).andExpect(view().name("success"))                .andDo(print());    }    @Test    public void testReturnPagePut() throws Exception    {        mockMvc.perform(put("/v1/putRequest/1")).andExpect(status().isOk()).andExpect(view().name("success"))                .andDo(print());    }    @Test    public void testRealApiGetUsers() throws Exception    {        // .accept方法是设置客户端可识别的内容类型        // .contentType,设置请求头中的Content-Type字段,表示请求体的内容类型        mockMvc.perform(get("/v1/getUsers").accept(MediaType.APPLICATION_JSON_UTF8)                .contentType(MediaType.APPLICATION_JSON_UTF8).param("page", "1").param("rows", "10")).andDo(print())                .andExpect(status().isOk()).andExpect(content().contentType("application/json;charset=UTF-8"))                .andExpect(jsonPath("$.total").exists()).andExpect(jsonPath("$.rows").isArray())                .andExpect(jsonPath("$.rows", hasSize(10)));    }    @Test    public void test001RealApiAddUser() throws Exception    {        MvcResult ret = mockMvc                .perform(post("/v1/users").accept(MediaType.APPLICATION_JSON_UTF8)                        .contentType(MediaType.APPLICATION_JSON_UTF8).param("name", "user1").param("age", "23")                        .param("gender", "女").param("email", "user1@126.com").param("teacherId", "1")                        .param("createTime", "2017-06-08 13:59:12").param("loginTime", "2017-06-08 13:59:12"))                .andDo(print()).andExpect(status().isOk())                .andExpect(content().contentType("application/json;charset=UTF-8"))                .andExpect(jsonPath("$.retCode").value(0)).andExpect(jsonPath("$.newId").exists()).andReturn();        MockHttpServletResponse resp = ret.getResponse();        String strContent = resp.getContentAsString();        JSONObject jsonObject = JSONObject.parseObject(strContent);        lastNewId = jsonObject.get("newId").toString();    }    @Test    public void test002RealApiUpdateUser() throws Exception    {        mockMvc.perform(put("/v1/users/" + lastNewId).accept(MediaType.APPLICATION_JSON_UTF8)                .contentType(MediaType.APPLICATION_JSON_UTF8).param("name", "user1").param("age", "33")                .param("gender", "男").param("email", "user1@126.com").param("teacherId", "2")                .param("createTime", "2017-06-08 13:59:12").param("loginTime", "2017-06-08 13:59:12")).andDo(print())                .andExpect(status().isOk()).andExpect(content().contentType("application/json;charset=UTF-8"))                .andExpect(jsonPath("$.retCode").value(0)).andExpect(jsonPath("$.status").value("更新成功"));    }    @Test    public void test003RealApiDeleteUser() throws Exception    {        mockMvc.perform(delete("/v1/users/" + lastNewId).accept(MediaType.APPLICATION_JSON_UTF8)                .contentType(MediaType.APPLICATION_JSON_UTF8)).andDo(print()).andExpect(status().isOk())                .andExpect(content().contentType("application/json;charset=UTF-8"))                .andExpect(jsonPath("$.retCode").value(0)).andExpect(jsonPath("$.status").value("删除成功"));    }}

现在service测试,和真实controller测试都是可以的,唯一不行的就是模拟controller的测试无法通过,因为不认识自定义的RequestMappingHandlerMapping,找了半天,找到个说法。目前没有暴露这个接口出来,会考虑在5.0RC1里面添加支持。
https://jira.spring.io/browse/SPR-15472

Rossen Stoyanchev added a comment - 25/Apr/17 2:01 PM - editedSimilar request here http://stackoverflow.com/questions/43102003/custom-requestmappinghandlermapping-with-mockmvc.The options on StandaloneMockMvcBuilder aim to be equivalent to what is exposed through WebMvcConfigurer. The custom HandlerMapping is more of an advanced option.That said I'll consider possible options to expose this for the standalone setup for 5.0 RC1.

修改index.jsp增加对不同版本的测试

<body>    <a href="v1/users">Get Users</a>    <br />    <br />    <form action="v1/putRequest/1" method="post">        <input type="hidden" name="_method" value="PUT" /> <input            type="submit" value="testRest PUT" />    </form>    <br />    <br />    <form action="v1/deleteRequest/1" method="post">        <input type="hidden" name="_method" value="DELETE" /> <input            type="submit" value="testRest DELETE" />    </form>    <br />    <br />    <form action="v1/postRequest" method="POST">        <input type="submit" text="Post Request">    </form>    <br>    <br>    <a href="v1/getRequest/1">Hello World!</a>    <br>    <br>    <a href="v3/getRequest/1">Hello World! V3</a>    <br>    <br>    <a href="v5/getRequest/1">Hello World! V5</a>    <br>    <br>    <a href="v7/getRequest/1">Hello World! V7</a></body>

011 RESTful接口优化

视频分享到youtube上了。
https://youtu.be/2oNGCB_j6V0
优酷链接

以下RESTful API 设计参考,摘录至网络:下面是链接
http://www.ruanyifeng.com/blog/2014/05/restful_api.html
RESTful API 设计参考:

  • 协议:使用Https协议
  • 域名:应该尽量将API部署在专用域名之下。
    https://api.example.com
  • 版本:应该将API的版本号放入URL
    https://api.example.com/v1/
  • 路径(Endpoint):表示API的具体网址
    在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的”集合”(collection),所以API中的名词也应该使用复数。
https://api.example.com/v1/zooshttps://api.example.com/v1/animalshttps://api.example.com/v1/employees
  • HTTP动词:
GET(SELECT):从服务器取出资源(一项或多项)。POST(CREATE):在服务器新建一个资源。PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。DELETEDELETE):从服务器删除资源。

还有两个不常用的HTTP动词。

HEAD:获取资源的元数据。OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

下面是一些例子。

GET /zoos:列出所有动物园POST /zoos:新建一个动物园GET /zoos/ID:获取某个指定动物园的信息PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)DELETE /zoos/ID:删除某个动物园GET /zoos/ID/animals:列出某个指定动物园的所有动物DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物
  • 过滤信息(Filtering)
    如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。下面是一些常见的参数。
?limit=10:指定返回记录的数量?offset=10:指定返回记录的开始位置。?page=2&per_page=100:指定第几页,以及每页的记录数。?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。?animal_type_id=1:指定筛选条件

参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。

  • 状态码(Status Codes)
    服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)204 NO CONTENT - [DELETE]:用户删除数据成功。400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

状态码的完全列表:
https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

  • 错误处理(Error handling)
    如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。
{    error: "Invalid API key"}
  • 返回结果

针对不同操作,服务器向用户返回的结果应该符合以下规范。

GET /collection:返回资源对象的列表(数组)GET /collection/resource:返回单个资源对象POST /collection:返回新生成的资源对象PUT /collection/resource:返回完整的资源对象PATCH /collection/resource:返回完整的资源对象DELETE /collection/resource:返回一个空文档
  • 其他

API的身份认证应该使用OAuth 2.0框架。
服务器返回的数据格式,应该尽量使用JSON,避免使用XML

为了解决某个添加用户的接口,由于业务导致字段属性变化很大,只能重新定义一个新的接口,由 /v1/users 变成了 /v2/users,这样我们就要维护两套接口的逻辑。同时如果使用更高的版本访问例如:v4/users访问,还可以自动适配到最新的v2版本。这里的自动适配要打个问号?这么做可能会产生不期望的后果。
参考自下面的链接:http://www.cnblogs.com/jcli/p/springmvc_restful_version.html

创建utils包,添加ApiVersion类

import java.lang.annotation.ElementType;import java.lang.annotation.RetentionPolicy;/** *  * 接口版本标识注解 * */@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Mappingpublic @interface ApiVersion {    int value();}

添加ApiVesrsionCondition类

public class ApiVesrsionCondition implements RequestCondition<ApiVesrsionCondition> {    // 路径中版本的前缀, 这里用 /v[1-9]/的形式    private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/");    private int apiVersion;    public ApiVesrsionCondition(int apiVersion){        this.apiVersion = apiVersion;    }    public ApiVesrsionCondition combine(ApiVesrsionCondition other) {        // 采用最后定义优先原则,则方法上的定义覆盖类上面的定义        return new ApiVesrsionCondition(other.getApiVersion());    }    public ApiVesrsionCondition getMatchingCondition(HttpServletRequest request) {        String requestUrl = request.getRequestURL().toString();//得到请求的URL地址         String requestUri = request.getRequestURI();//得到请求的资源        Matcher m = VERSION_PREFIX_PATTERN.matcher(requestUri);        if(m.find()){            Integer version = Integer.valueOf(m.group(1));            if(version >= this.apiVersion) // 如果请求的版本号大于配置版本号, 则满足                return this;        }        return null;    }    public int compareTo(ApiVesrsionCondition other, HttpServletRequest request) {        // 优先匹配最新的版本号        return other.getApiVersion() - this.apiVersion;    }    public int getApiVersion() {        return apiVersion;    }}

添加CustomRequestMappingHandlerMapping类

@RequestMappingpublic class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {    @Override    protected RequestCondition<ApiVesrsionCondition> getCustomTypeCondition(Class<?> handlerType) {        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);        return createCondition(apiVersion);    }    @Override    protected RequestCondition<ApiVesrsionCondition> getCustomMethodCondition(Method method) {        ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);        return createCondition(apiVersion);    }    private RequestCondition<ApiVesrsionCondition> createCondition(ApiVersion apiVersion) {        return apiVersion == null ? null : new ApiVesrsionCondition(apiVersion.value());    }}

为了实现这个功能,需要使用java Config配置方式自定义RequestMappingHandlerMapping

这里有个参考迁移到Java Based Configuration的链接
https://www.luckyryan.com/2013/02/07/migrate-spring-mvc-servlet-xml-to-java-config/

使用 JavaConfig 的好处,Spring 官方文档的描述:

  • JavaConfig 为依赖注入提供了一个真正面向对象的机制,这意味着开发者可以在配置代码中充分利用 Java 重用、继承和多态性等特性。
  • 开发者可以完全控制实例化和依赖注入,即使是最复杂的对象也可以很优雅地处理。
  • 因为使用 JavaConfig 只需要 Java,可以很容易的 refactor 代码,而无需再 IDE 之外使用特殊的工具或插件。

创建config包,在config包下面创建WebMvcConfig类,实现自定义RequestMappingHandlerMapping的功能,因为需要注释掉springmvc.xml里面的mvc:annotation-driven所以在这里配置消息转换处理fastjson

@Configuration@EnableWebMvcpublic class WebMvcConfig extends WebMvcConfigurerAdapter{    @Autowired    private DateTimeConverter dateTimeConverter;    public void addFormatters(FormatterRegistry registry)    {        registry.addConverter(dateTimeConverter);    }    // equivalents for <mvc:resources/> tags    @Override    public void addResourceHandlers(final ResourceHandlerRegistry registry)    {        CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS);        registry.addResourceHandler("/resources/**").addResourceLocations("/resources/commons/")                .setCachePeriod(31556926).addResourceLocations("/resources/themeDefault/").setCachePeriod(31556926)                .setCacheControl(cc);        registry.setOrder(0);    }    @Bean    public StringHttpMessageConverter getStringHttpMessageConverter()    {        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(Charset.forName("UTF-8"));        List<MediaType> list = new ArrayList<MediaType>();        list.add(new MediaType("text", "plain", Charset.forName("UTF-8")));        list.add(new MediaType("*", "*", Charset.forName("UTF-8")));        stringConverter.setSupportedMediaTypes(list);        return stringConverter;    }    @Bean    public RequestMappingHandlerMapping requestMappingHandlerMapping()    {        RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();        handlerMapping.setOrder(0);        // handlerMapping.setInterceptors(getInterceptors());        return handlerMapping;    }    @Bean    public FastJsonHttpMessageConverter getFastJson()    {        FastJsonHttpMessageConverter jsonConverter = new FastJsonHttpMessageConverter();        List<MediaType> jsonList = new ArrayList<MediaType>();        jsonList.add(MediaType.valueOf("application/json;charset=UTF-8"));        jsonList.add(MediaType.valueOf("text/plain;charset=utf-8"));        jsonList.add(MediaType.valueOf("text/html;charset=utf-8"));        jsonConverter.setSupportedMediaTypes(jsonList);        FastJsonConfig fastJsonConfig = new FastJsonConfig();        fastJsonConfig.setCharset(Charset.forName("UTF-8"));        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat, SerializerFeature.WriteMapNullValue);        ValueFilter valueFilter = new ValueFilter()        {            public Object process(Object classRef, String key, Object value)            {                if (null == value)                {                    value = "";                }                return value;            }        };        fastJsonConfig.setSerializeFilters(valueFilter);        jsonConverter.setFastJsonConfig(fastJsonConfig);        return jsonConverter;    }    public void configureMessageConverters(List<HttpMessageConverter<?>> converters)    {        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(Charset.forName("UTF-8"));        List<MediaType> list = new ArrayList<MediaType>();        list.add(new MediaType("text", "plain", Charset.forName("UTF-8")));        list.add(new MediaType("*", "*", Charset.forName("UTF-8")));        stringConverter.setSupportedMediaTypes(list);        FastJsonHttpMessageConverter4 jsonConverter = new FastJsonHttpMessageConverter4();        List<MediaType> jsonList = new ArrayList<MediaType>();        jsonList.add(MediaType.valueOf("application/json;charset=UTF-8"));        jsonList.add(MediaType.valueOf("text/plain;charset=utf-8"));        jsonList.add(MediaType.valueOf("text/html;charset=utf-8"));        jsonConverter.setSupportedMediaTypes(jsonList);        FastJsonConfig fastJsonConfig = new FastJsonConfig();        fastJsonConfig.setCharset(Charset.forName("UTF-8"));        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);        jsonConverter.setFastJsonConfig(fastJsonConfig);        converters.add(stringConverter);        converters.add(jsonConverter);    }}

springmvc.xml还需要做修改,把config包加进去

<!-- 配置自动扫描的包 --><context:component-scan    base-package="com.study.controller,com.study.service,com.study.converters,com.study.config"></context:component-scan>

同时还要注释掉,包括上面的bean声明,这里的fastjson使用Java config方式添加配置,也可以注释掉了,在java config里面配置了。

<!-- <mvc:annotation-driven conversion-service="conversionService">    <mvc:message-converters register-defaults="true">        <ref bean="stringHttpMessageConverter" />        <ref bean="fastJsonHttpMessageConverter" />    </mvc:message-converters></mvc:annotation-driven> -->

在controller的路径映射里面添加版本信息v1,参考如下,同时修改对应的测试类。增加新的版本后,可以在方法里面覆盖类上面的版本。这个挺有用的,不用一个一个的写了。

@RequestMapping("/{version}/")@ApiVersion(1)@Controllerpublic class UserController{...    @ApiVersion(3)    @RequestMapping("/getRequest/{id}")    public String getRequestProc3(@PathVariable("id") Integer id,HttpServletRequest request, HttpServletResponse response)    {        System.out.println("getRequestProc3: " + id);        return "success";    }    @ApiVersion(5)    @RequestMapping("getRequest/{id}")    public String getRequestProc5(@PathVariable("id") Integer id)    {        System.out.println("getRequestProc5: " + id);        return "success";    }    ...

修改TeacherController增加版本信息

修改users.jsp,主要是静态资源的相对路径,和url的版本信息

<%@ page language="java" contentType="text/html; charset=UTF-8"    pageEncoding="UTF-8"%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>Insert title here</title><!-- 下面两个必须引用 --><script type="text/javascript"    src="../resources/jsExt/jquery-easyui-1.5.2/jquery.min.js"></script><script type="text/javascript"    src="../resources//jsExt/jquery-easyui-1.5.2/jquery.easyui.min.js"></script><!-- 国际化文件 --><script type="text/javascript"    src="../resources//jsExt/jquery-easyui-1.5.2/locale/easyui-lang-zh_CN.js"></script><!-- 默认样式 --><link rel="stylesheet"    href="../resources/jsExt/jquery-easyui-1.5.2/themes/default/easyui.css"><!-- 图标 --><link rel="stylesheet"    href="../resources/jsExt/jquery-easyui-1.5.2/themes/icon.css"><!-- 自定义js --><script type="text/javascript" src="../resources/js/teacherFormatter.js"></script><!-- 自定义的js脚本 --><script type="text/javascript" src="../resources/js/commons.js"></script><script type="text/javascript" src="../resources/js/user/userOper.js"></script><link rel="stylesheet" href="../resources/css/user.css"></head><body>    <div id="tt" class="easyui-layout" style="margin: 20px 20px 20px 20px;">        <table id="dg" class="easyui-datagrid" title="用户列表"            style="width: 100%; height: 400px;pagination-num{width:200px}"            data-options="rownumbers:true,striped:true,fitColumns:true,singleSelect:true,autoRowHeight:true,pagination:true,                pageSize:12,pageList:[12,100,200,300],url:'${pageContext.request.contextPath}/v1/getUsers',method:'get',toolbar:'#toolbar'">            <thead>                <tr>                    <th data-options="field:'id',width:100">ID</th>                    <th data-options="field:'name',width:100">name</th>                    <th data-options="field:'age',width:100">Age</th>                    <th data-options="field:'gender',width:60">Gender</th>                    <th data-options="field:'email',width:150">Email</th>                    <th data-options="field:'createTime',width:150">Create Time</th>                    <th data-options="field:'loginTime',width:150">Login Time</th>                    <th data-options="field:'teacher',width:100"                        formatter="teacherFormatter">Teacher Name</th>                </tr>            </thead>        </table>        <div id="toolbar">            <a href="#" class="easyui-linkbutton"                data-options="iconCls:'icon-add'" onclick="insert()">添加</a> <a                href="#" class="easyui-linkbutton"                data-options="iconCls:'icon-edit'" onclick="edit()">编辑</a> <a                href="#" class="easyui-linkbutton"                data-options="iconCls:'icon-remove'" onclick="del()">删除</a>        </div>    </div>    <!-- create user dialog -->    <div id="dlg" class="easyui-dialog"        data-options="iconCls:'icon-save',resizable:true,modal:true"        style="width: 400px; height: 280px; padding: 10px 20px" closed="true"        buttons="#dlg-buttons">        <div class="ftitle">用户信息</div>        <form id="fm" method="post">            <div class="fitem">                <label for="name">用户名:</label> <input name="name"                    class="easyui-validatebox" type="text" data-options="required:true">            </div>            <div class="fitem">                <label for="age">年 龄:</label> <input name="age"                    class="easyui-numberbox" type="text"                    data-options="required:true,validType:'number'">            </div>            <div class="fitem">                <label for="gender">性 别:</label> <input id="state1" name="gender"                    value="男" type="radio" checked="true" /><input id="state2"                    name="gender" value="女" type="radio" /></div>            <div class="fitem">                <label for="email">Email:</label> <input name="email"                    class="easyui-validatebox" type="text"                    data-options="required:true,validType:'email'">            </div>            <div class="fitem">                <label for="teacherId">教 师:</label> <input id="cc"                    class="easyui-combobox" name="teacherId"                    data-options="valueField:'id',textField:'name',panelHeight:80,editable:false,method:'get',url:'${pageContext.request.contextPath}/v1/getTeacherComboData'">            </div>        </form>    </div>    <div id="dlg-buttons">        <a href="#" class="easyui-linkbutton" data-options="iconCls:'icon-ok'"            onclick="save()">Save</a> <a href="#" class="easyui-linkbutton"            data-options="iconCls:'icon-cancel'"            onclick="javascript:$('#dlg').dialog('close')">Cancel</a>    </div></body></html>

修改userOper.js增加版本信息

/*! * imalex@163.com - v0.1 (2015-08-29 18:00:00 +0800) * Copyright 2015 */$(function() {});$('#dg').pagination({    pageSize: 20,//每页显示的记录条数,默认为10     pageList: [20, 50, 100],//可以设置每页记录条数的列表 });function insert() {    $('#dlg').dialog('open').dialog('setTitle', '新建用户');    $('#fm').form('clear');    url = ctx + '/v1/users';    type = insert;}function edit() {    var row = $('#dg').datagrid('getSelected');    if (row) {        $('#dlg').dialog('open').dialog('setTitle', '编辑用户信息');        $('#fm').form('load', row);        //setCheckedValue(document.forms['fm'].elements['gender'], row['gender']);        url = ctx + '/v1/users/' + row.id;        type = edit;    }}function save() {    $('#fm').form('submit', {        url : url,        type:type,        onSubmit : function(param) {            if (type == edit) {                //use put                 param._method ='PUT';            }            param.createTime = new Date().Format("yyyy-MM-dd hh:mm:ss");            param.loginTime = param.createTime;            var ret = $(this).form('validate');            return ret;        },        success : function(result) {            var result = eval('(' + result + ')');            if (result.errorMsg) {                $.messager.show({                    title : 'Error',                    msg : result.errorMsg                });            } else {                $('#dlg').dialog('close'); // close the dialog                $('#dg').datagrid('reload'); // reload the user data            }        }    });}function del() {    var row = $('#dg').datagrid('getSelected');    if (row) {        $.messager.confirm('确认', '请确认删除已选择用户?', function(r) {            if (r) {                $.post(ctx + '/v1/users/' + row.id, {                    _method : 'DELETE'                }, function(result) {                    if (result.status != null) {                        $('#dg').datagrid('reload'); // reload the user data                    } else {                        $.messager.show({ // show error message                            title : 'Error',                            msg : result.errorMsg                        });                    }                }, 'json');            }        });    }}

测试类补齐TestUserController里面的路径测试

@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:springmvc.xml")@WebAppConfiguration@FixMethodOrder(MethodSorters.NAME_ASCENDING)public class TestUserController{    public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(),            MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));    @Autowired    private WebApplicationContext wac;    private MockMvc mockMvc;    private static String lastNewId;    @Before    public void init()    {        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();    }    @Test    public void testReturnPage() throws Exception    {        mockMvc.perform(get("/v1/users")).andExpect(status().isOk()).andExpect(view().name("users")).andDo(print());    }    @Test    public void testReturnPageGet() throws Exception    {        mockMvc.perform(get("/v1/getRequest/1")).andExpect(status().isOk()).andExpect(view().name("success"))                .andDo(print());    }    @Test    public void testReturnPagePost() throws Exception    {        mockMvc.perform(post("/v1/postRequest")).andExpect(status().isOk()).andExpect(view().name("success"))                .andDo(print());    }    @Test    public void testReturnPageDelete() throws Exception    {        mockMvc.perform(delete("/v1/deleteRequest/1")).andExpect(status().isOk()).andExpect(view().name("success"))                .andDo(print());    }    @Test    public void testReturnPagePut() throws Exception    {        mockMvc.perform(put("/v1/putRequest/1")).andExpect(status().isOk()).andExpect(view().name("success"))                .andDo(print());    }    @Test    public void testRealApiGetUsers() throws Exception    {        // .accept方法是设置客户端可识别的内容类型        // .contentType,设置请求头中的Content-Type字段,表示请求体的内容类型        mockMvc.perform(get("/v1/getUsers").accept(MediaType.APPLICATION_JSON_UTF8)                .contentType(MediaType.APPLICATION_JSON_UTF8).param("page", "1").param("rows", "10")).andDo(print())                .andExpect(status().isOk()).andExpect(content().contentType("application/json;charset=UTF-8"))                .andExpect(jsonPath("$.total").exists()).andExpect(jsonPath("$.rows").isArray())                .andExpect(jsonPath("$.rows", hasSize(10)));    }    @Test    public void test001RealApiAddUser() throws Exception    {        MvcResult ret = mockMvc                .perform(post("/v1/users").accept(MediaType.APPLICATION_JSON_UTF8)                        .contentType(MediaType.APPLICATION_JSON_UTF8).param("name", "user1").param("age", "23")                        .param("gender", "女").param("email", "user1@126.com").param("teacherId", "1")                        .param("createTime", "2017-06-08 13:59:12").param("loginTime", "2017-06-08 13:59:12"))                .andDo(print()).andExpect(status().isOk())                .andExpect(content().contentType("application/json;charset=UTF-8"))                .andExpect(jsonPath("$.retCode").value(0)).andExpect(jsonPath("$.newId").exists()).andReturn();        MockHttpServletResponse resp = ret.getResponse();        String strContent = resp.getContentAsString();        JSONObject jsonObject = JSONObject.parseObject(strContent);        lastNewId = jsonObject.get("newId").toString();    }    @Test    public void test002RealApiUpdateUser() throws Exception    {        mockMvc.perform(put("/v1/users/" + lastNewId).accept(MediaType.APPLICATION_JSON_UTF8)                .contentType(MediaType.APPLICATION_JSON_UTF8).param("name", "user1").param("age", "33")                .param("gender", "男").param("email", "user1@126.com").param("teacherId", "2")                .param("createTime", "2017-06-08 13:59:12").param("loginTime", "2017-06-08 13:59:12")).andDo(print())                .andExpect(status().isOk()).andExpect(content().contentType("application/json;charset=UTF-8"))                .andExpect(jsonPath("$.retCode").value(0)).andExpect(jsonPath("$.status").value("更新成功"));    }    @Test    public void test003RealApiDeleteUser() throws Exception    {        mockMvc.perform(delete("/v1/users/" + lastNewId).accept(MediaType.APPLICATION_JSON_UTF8)                .contentType(MediaType.APPLICATION_JSON_UTF8)).andDo(print()).andExpect(status().isOk())                .andExpect(content().contentType("application/json;charset=UTF-8"))                .andExpect(jsonPath("$.retCode").value(0)).andExpect(jsonPath("$.status").value("删除成功"));    }}

现在service测试,和真实controller测试都是可以的,唯一不行的就是模拟controller的测试无法通过,因为不认识自定义的RequestMappingHandlerMapping,找了半天,找到个说法。目前没有暴露这个接口出来,会考虑在5.0RC1里面添加支持。
https://jira.spring.io/browse/SPR-15472

Rossen Stoyanchev added a comment - 25/Apr/17 2:01 PM - editedSimilar request here http://stackoverflow.com/questions/43102003/custom-requestmappinghandlermapping-with-mockmvc.The options on StandaloneMockMvcBuilder aim to be equivalent to what is exposed through WebMvcConfigurer. The custom HandlerMapping is more of an advanced option.That said I'll consider possible options to expose this for the standalone setup for 5.0 RC1.

修改index.jsp增加对不同版本的测试

<body>    <a href="v1/users">Get Users</a>    <br />    <br />    <form action="v1/putRequest/1" method="post">        <input type="hidden" name="_method" value="PUT" /> <input            type="submit" value="testRest PUT" />    </form>    <br />    <br />    <form action="v1/deleteRequest/1" method="post">        <input type="hidden" name="_method" value="DELETE" /> <input            type="submit" value="testRest DELETE" />    </form>    <br />    <br />    <form action="v1/postRequest" method="POST">        <input type="submit" text="Post Request">    </form>    <br>    <br>    <a href="v1/getRequest/1">Hello World!</a>    <br>    <br>    <a href="v3/getRequest/1">Hello World! V3</a>    <br>    <br>    <a href="v5/getRequest/1">Hello World! V5</a>    <br>    <br>    <a href="v7/getRequest/1">Hello World! V7</a></body>

现在可以访问主页测试不同版本的功能了。

原创粉丝点击