springmvc的restful api 增加版本参数
2019-06-23 本文已影响0人
有时右逝
前言
最近在为一个客户端实现api接口,客户端提出一个问题,接口没有版本吗?我一时语塞,虽然知道应该有版本,但是实际应用中没实现有版本的情况。特此花费时间解决这个问题。
过程
当前写接口,是基于ssm结构来构建的。
以登录接口为例。
原本的路由是post /user/login
期望的路由是post /version/1/token
这里restful api要求接口通常是名词,因此这里一并纠正。以前的工作是多么的粗糙。
- 编写注解
package com.wuwenfu.aily.base;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.Mapping;
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
@Component
public @interface ApiVersion {
/**
* 标识版本号
* @return
*/
int value();
}
- 引入注解
package com.wuwenfu.aily.controller;
import com.wuwenfu.aily.base.ApiVersion;
import com.wuwenfu.aily.base.BaseController;
import com.wuwenfu.aily.result.Response;
import com.wuwenfu.aily.service.UserService;
import com.wuwenfu.aily.service.YzmService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* @ClassName UserController
* @Description TODO
* @Author hi_php@163.com
* @Date Tue Jun 11 13:35:03 CST 2019
*/
@Api(value = "用户接口", description = "")
@RestController
@RequestMapping("/version/{version}")
@Slf4j
public class TokenController extends BaseController {
/**
* 登录
*/
@ApiOperation("登录")
@ApiVersion(1)
@RequestMapping(value = "/token", method = RequestMethod.POST)
public Response login() {
return new Response().success("");
}
/**
* 登录-版本2
*/
@ApiOperation("登录-版本2")
@ApiVersion(2)
@RequestMapping(value = "/token", method = RequestMethod.POST)
public Response login2() {
return new Response().success("");
}
}
- 路由转换
上面的注解要能生效,需要对访问路由进行识别。
package com.wuwenfu.aily.base;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ApiVersionRequestCondition implements RequestCondition<ApiVersionRequestCondition> {
// 用于匹配request中的版本号 v1 v2
private static final Pattern VERSION_PATTERN = Pattern.compile("/version/(\\d+).*");
// 保存当前的版本号
private int version;
// 保存所有接口的最大版本号
private static int maxVersion = 1;
public ApiVersionRequestCondition(int version) {
this.version = version;
}
@Override
public ApiVersionRequestCondition combine(ApiVersionRequestCondition other) {
// 上文的getMappingForMethod方法中是使用 类的Condition.combine(方法的condition)的结果
// 确定一个方法的condition,所以偷懒的写法,直接返回参数的版本,可以保证方法优先,可以优化
// 在condition中增加一个来源于类或者方法的标识,以此判断,优先整合方法的condition
return new ApiVersionRequestCondition(other.version);
}
@Override
public ApiVersionRequestCondition getMatchingCondition(HttpServletRequest request) {
// 正则匹配请求的uri,看是否有版本号 v1
Matcher matcher = VERSION_PATTERN.matcher(request.getRequestURI());
if (matcher.find()) {
String versionNo = matcher.group(1);
int version = Integer.valueOf(versionNo);
// 超过当前最大版本号或者低于最低的版本号均返回不匹配
if (version <= maxVersion && version >= this.version) {
return this;
}
}
return null;
}
@Override
public int compareTo(ApiVersionRequestCondition other, HttpServletRequest request) {
// 以版本号大小判定优先级,越高越优先
return other.version - this.version;
}
public int getVersion() {
return version;
}
public static void setMaxVersion(int maxVersion) {
ApiVersionRequestCondition.maxVersion = maxVersion;
}
}
package com.wuwenfu.aily.base;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping implements PriorityOrdered {
private int latestVersion = 1;
@Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
// 判断是否有@ApiVersion注解,构建基于@ApiVersion的RequestCondition
ApiVersionRequestCondition condition = buildFrom(AnnotationUtils.findAnnotation(handlerType, ApiVersion.class));
// 保存最大版本号
if (condition != null && condition.getVersion() > latestVersion) {
ApiVersionRequestCondition.setMaxVersion(condition.getVersion());
}
return condition;
}
@Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
// 判断是否有@ApiVersion注解,构建基于@ApiVersion的RequestCondition
ApiVersionRequestCondition condition = buildFrom(AnnotationUtils.findAnnotation(method, ApiVersion.class));
// 保存最大版本号
if (condition != null && condition.getVersion() > latestVersion) {
ApiVersionRequestCondition.setMaxVersion(condition.getVersion());
}
return condition;
}
private ApiVersionRequestCondition buildFrom(ApiVersion apiVersion) {
return apiVersion == null ? null : new ApiVersionRequestCondition(apiVersion.value());
}
}
- 启用配置
默认的spring-mvc的路由映射关系需要修改,否则无法调用上面自定义的映射逻辑。
package com.wuwenfu.aily.controller;
import com.wuwenfu.aily.base.CustomRequestMappingHandlerMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurationSupport {
@Override
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
handlerMapping.setInterceptors(getInterceptors());
return handlerMapping;
}
}
- 配置文件
我的项目存在3个配置文件。
web.xml 、spring.xml 、spring-mvc.xml
web.xml如下
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_5.xsd">
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- 创建log4j日志监听 -->
<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener
</listener-class>
</listener>
<!-- log4jConfigLocation:log4j配置文件存放路径 -->
<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>classpath:log4j.xml</param-value>
</context-param>
<!-- 创建spring监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!-- spring的监听器可以通过这个上下文参数来获取applicationContext.xml的位置 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml;classpath:spring-shiro.xml</param-value>
</context-param>
<servlet>
<servlet-name>spring-mvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring-mvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>httpPutFormcontentFilter</filter-name>
<filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>httpPutFormcontentFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>CharacterFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterFilter</filter-name>
<url-pattern>*</url-pattern>
</filter-mapping>
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
spring.xml的配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:property-placeholder location="classpath*:*.properties" />
<!--数据库配置-->
<import resource="spring-mybatis.xml"/>
<import resource="rabbitmq.xml"/>
<!--扫描包-排除controller-->
<context:component-scan base-package="com.wuwenfu.aily">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<bean class="redis.clients.jedis.JedisPool" id="jedisPool">
<constructor-arg name="host" value="${redis.host}"></constructor-arg>
<constructor-arg name="port" value="${redis.port}"></constructor-arg>
<constructor-arg name="password" value="${redis.password}"></constructor-arg>
<constructor-arg name="timeout" value="${redis.timeout}"></constructor-arg>
<constructor-arg name="database" value="${redis.database}"></constructor-arg>
<constructor-arg name="poolConfig" ref="jedisPoolConfig"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.JedisPoolConfig" id="jedisPoolConfig">
<property name="maxIdle" value="${maxIdle}" />
<property name="maxTotal" value="${maxTotal}" />
<property name="maxWaitMillis" value="${maxWait}" />
<property name="testOnBorrow" value="${testOnBorrow}" />
<property name="blockWhenExhausted" value="${blockWhenExhausted}" />
</bean>
<task:annotation-driven/>
</beans>
spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--<context:property-placeholder location="classpath*:*.properties" />-->
<context:annotation-config />
<context:component-scan base-package="com.wuwenfu.aily" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
<context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.RestController" />
</context:component-scan>
<!--使用默认的Servlet来响应静态文件-->
<mvc:default-servlet-handler />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- 上传文件拦截,设置最大上传文件大小 10M=10*1024*1024(B)=10485760 bytes -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="10485760" />
</bean>
<!--执行切面拦截-->
<aop:aspectj-autoproxy expose-proxy="true"></aop:aspectj-autoproxy>
<!-- 引入swagger相关 -->
<bean class="com.wuwenfu.aily.doc.MySwaggerConfig"/>
<mvc:resources mapping="swagger-ui.html" location="classpath:/META-INF/resources/" />
<mvc:resources location="classpath:/META-INF/resources/webjars/" mapping="/webjars/**"/>
<bean class="springfox.documentation.swagger2.configuration.Swagger2DocumentationConfiguration" id="swagger2Config"/>
<context:component-scan base-package="com.wuwenfu.aily.controller" ></context:component-scan>
</beans>
这里最重要的部分是不要启用
<mvc:annotation-driven></mvc:annotation-driven> 否则自定义的映射路由无法启动。
- 测试
使用postman进行测试
image.png image.png