一、理论基础
1.1 如何实现一个相对健壮的接口
接口设计应该假设所有的调用者都是不靠谱的,所以需要做全方位的防御措施并尽可能考虑到各种因素
正常访问
一个接口能正常访问是最基本的、最低的要求。不管调用者传递什么参数,接口应该都能给予良好的反馈,即使参数是错的。
当用户参数传递错误时,应该将错误信息反馈给用户,比如缺少参数或者参数格式不正确等
返回值统一化
标准化的返回格式,是绝对有利于同事间的感情发展的。
如果你一会返回个S,一会返回个B,你一定会被诅咒成这个返回值的拼接体的(人工狗头)
返回该返回的
尽可能的精简返回值,不要返回过多的冗余信息,比如调用方只需要两个字段,如果返回几十个字段,对于调用者来讲使用起来也不是很方便,而且还可能会暴露内部的业务逻辑。
报错信息全局处理
最常见的一个场景就是接口调用错误会将内部代码返给调用者,遇到过一个报名页面报错后,直接返回了几行代码,而且因为报错的很多都是关键逻辑,连代码注释的作者都能看到。
所以未知的错误最好统一处理下,比如“网络错误“等
及时更新的文档
写文档是最不受待见的一件事,那么一个自动化接口文档就很必要了,Swagger虽然繁琐,但是用起来很香。
而且可以设置默认的一些参数值,直接调试接口,真的香。
版本管理
不管会不会迭代,最好加个版本号。一来可以让接口看起来像”正规军”,二来可以有效应对随时到来的需求变更。
健壮的业务逻辑
这个就不多说了,相信每个人都有自己的一套方法论。
1.2 本实例集成的功能
- 接口异常全局处理
- Knife4j接口文档
- 简单的token验证
- Validator参数校验
- 返回值的统一化
- 解决跨域问题
- AOP统计接口访问次数
1.3 示例源码地址
https://github.com/lysmile/spring-boot-demo/tree/master/spring-boot-api-handler-demo
二、接口异常全局处理
/*** 接口访问统一异常处理* - 当程序报错时,由此类进行统一处理* - 防止将程序内部错误暴露给用户* @author smile*/
@RestControllerAdvice
@Slf4j
public class ControllerExceptionHandleAdvice {@ExceptionHandler(ValidatorRuntimeException.class)public CodeResult validationExceptionHandler(HttpServletRequest request, ValidatorRuntimeException e) {if (log.isDebugEnabled()) {log.error("接口[{}]请求发生[接口检验异常],错误信息:{}", request.getRequestURI(), e.getMessage());}return new CodeResult(ResponseEnum.REQUEST_PARAM_ERROR.getCode(), e.getMessage());}@ExceptionHandler(Exception.class)public CodeResult exceptionHandler(HttpServletRequest request, Exception e) {e.printStackTrace();log.error("接口[{}]请求发生异常,错误信息:{}", request.getRequestURI(), e.getMessage());return new CodeResult(ResponseEnum.SERVICE_ERROR);}@ExceptionHandler(TokenException.class)public CodeResult exceptionHandler(HttpServletRequest request, TokenException e) {if (log.isDebugEnabled()) {log.error("接口[{}]请求发生[token验证异常],错误信息:{}, token:{}", request.getRequestURI(), e.getMessage(), request.getHeader("token"));}return new CodeResult(ResponseEnum.TOKEN_ERROR.getCode(), e.getMessage());}
}
三、Knife4j接口文档
官网文档:https://xiaoym.gitee.io/knife4j/
3.1 依赖引入
<!-- knife4j api文档 -->
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>2.0.7</version>
</dependency>
3.2 配置文件
knife4j:# 开启增强配置enable: true# 开启Swagger的Basic认证功能,默认是falsebasic:enable: trueusername: smilepassword: 123456# 配置文档路径documents:-group: 0.1name: 说明文档# 某一个文件夹下所有的.md文件locations: classpath:markdown/*-group: 0.1name: 说明文档2# 某一个文件夹下所有的.md文件locations: classpath:markdown/*
3.3 配置文件
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {private final OpenApiExtensionResolver openApiExtensionResolver;public Knife4jConfig(OpenApiExtensionResolver openApiExtensionResolver) {this.openApiExtensionResolver = openApiExtensionResolver;}@Bean(value = "defaultApi2")public Docket defaultApi2() {Docket docket = new Docket(DocumentationType.SWAGGER_2).apiInfo(new ApiInfoBuilder().title("Springboot API Handler").description("Springboot API 处理方法集成").version("0.1").build())//分组名称.groupName("0.1").select()//这里指定Controller扫描包路径.apis(RequestHandlerSelectors.basePackage("com.smile.demo.apihandler.controller")).paths(PathSelectors.any()).build()// 此处的0.1对应配置文件中的document-group.extensions(openApiExtensionResolver.buildExtensions("0.1"));return docket;}
}
3.4 简单应用
@Api(tags = "接口测试")
@RestController
@ApiSort(102)
@Slf4j
@RequestMapping("v0.1")
public class MainController {@ApiImplicitParam(name = "name", value = "姓名", required = true)@ApiOperation(value = "测试接口")@GetMapping("first-demo")public CodeResult firstDemo(@RequestParam(value = "name") String name) {log.info("!!成功进入接口, 参数是:[{}]", name);return new CodeResult(ResponseEnum.SUCCESS, name);}
}
完整代码可参考本实例源码
四、简单的token验证
本示例只实现了简单的token验证(验证用户和token是否过期)
优化方向
- Springboot全家桶:集成Springboot Security
- 自实现功能:Jwt + Redis可实现较为完善的Token验证功能
4.1 依赖引入
<!-- java-jwt -->
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.10.3</version>
</dependency>
4.2 配置文件
my:security:username: apipassword: 123456
4.3 Jwt工具类
@Component
public class JwtUtils {@Value("${my.security.username}")private String defaultUsername;@Value("${my.security.password}")private String defaultPassword;/*** 过期时间,单位毫秒* - 10分钟*/private static final long EXPIRE_TIME = 1000 * 60 * 10;public TokenInfo createToken(String username, String password) throws TokenException {if (!username.equals(defaultUsername) || !password.equals(defaultPassword)) {throw new TokenException("用户名或密码不正确");}//设置头信息HashMap<String, Object> header = new HashMap<>(2);header.put("typ", "JWT");header.put("alg", "HMAC256");long expireAt = System.currentTimeMillis() + EXPIRE_TIME;String token = JWT.create().withHeader(header)// 存入需要保存在token的信息.withAudience(username)// 过期时间.withExpiresAt(new Date(expireAt)).sign(Algorithm.HMAC256(password));return new TokenInfo(token, expireAt);}public void verify(String token) throws TokenException {Algorithm algorithm = Algorithm.HMAC256(defaultPassword);JWTVerifier verifier = JWT.require(algorithm).build();try {DecodedJWT jwt = verifier.verify(token);if(!defaultUsername.equals(jwt.getAudience().get(0))) {throw new TokenException("用户不存在");}} catch(TokenExpiredException e) {throw new TokenException("token已过期");} catch(Exception e) {e.printStackTrace();throw new TokenException("token无效");}}
}
4.4 拦截器
4.4.1 拦截器功能实现
/*** 拦截器* - 验证token* @author yangjunqiang*/
@Component
@Slf4j
public class UserTokenInterceptor implements HandlerInterceptor {private final JwtUtils jwtUtils;public UserTokenInterceptor(JwtUtils jwtUtils) {this.jwtUtils = jwtUtils;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("!!进入拦截器方法");String token = request.getHeader("token");if (StringUtils.isBlank(token)) {throw new TokenException("请传入正确的token");}jwtUtils.verify(token);return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}
4.4.2注册拦截器
@Component
public class UserTokenAppConfig implements WebMvcConfigurer {private final UserTokenInterceptor userTokenInterceptor;public UserTokenAppConfig(UserTokenInterceptor userTokenInterceptor) {this.userTokenInterceptor = userTokenInterceptor;}@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(userTokenInterceptor)// 配置需要被拦截的接口// 注意此处是controller中的路径,配置文件中的server.servlet.context-path不能在此处加上,否则拦截器不会生效.addPathPatterns("/v0.1/**")// 不被拦截的接口.excludePathPatterns("/token/get");}
}
五、Validator参数校验
5.1 依赖引入
<!-- 参数检验 -->
<dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId>
</dependency>
5.2 快速失败配置(可选)
/*** 参数校验快速失败配置* - 此模式下只要发现一个参数不匹配便会快速返回错误* @author smile*/
@Configuration
public class ValidatorConfig {@Beanpublic Validator validator() {ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class ).configure().failFast( true ).buildValidatorFactory();Validator validator = validatorFactory.getValidator();return validator;}
}
5.3 工具类
/*** 参数验证通用方法提取* @author smile*/
public class ValidatorUtils {/*** controller bindingResult处理* @param bindingResult 参数验证结果集*/public static void handleBindingResult(BindingResult bindingResult) {if (bindingResult.hasErrors()) {throw new ValidatorRuntimeException(bindingResult.getAllErrors().get(0).getDefaultMessage());}}
}
5.4 简单应用
关键词:@Validated、BindingResult
配合全局异常处理一块使用
@PostMapping("get")
@ApiOperationSupport(author = "smile")
@ApiOperation(value = "获取token")
public CodeResult getToken(@RequestBody @Validated User user, BindingResult bindingResult) throws TokenException {ValidatorUtils.handleBindingResult(bindingResult);return new CodeResult(ResponseEnum.SUCCESS, JSON.toJSON(jwtUtils.createToken(user.getUsername(), user.getPassword())));
}
六、接口返回值统一化
6.1 接口返回实体定义
@Data
public class CodeResult {private String errCode;private String errMsg;private Object data;public CodeResult() { }public CodeResult(String errCode, String errMsg) {this.errCode = errCode;this.errMsg = errMsg;}public CodeResult(ResponseEnum response) {this.errCode = response.getCode();this.errMsg = response.getMsg();}public CodeResult(ResponseEnum response, Object data) { this.errCode = response.getCode();this.errMsg = response.getMsg();this.data = data;}}
定义枚举
/*** 错误码* 0 :成功* 1*:业务内自定义错误码* 2*:* 3*:* 4*:网络错误* 5*:系统内部错误,包含代码执行异常等* @author smile**/
public enum ResponseEnum {/*** 请求成功*/SUCCESS("0", "success"),/*** 请求参数错误*/REQUEST_PARAM_ERROR("1001", "参数错误"),/*** token验证失败*/TOKEN_ERROR("1002", "token验证失败"),/*** 服务内部异常,包括代码执行错误及一些不确定错误*/SERVICE_ERROR("5001", "服务异常,请稍后再试!");private String code;private String msg;ResponseEnum(String code, String msg) {this.code = code;this.msg = msg;}public String getCode() {return code;}public void setCode(String code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}}
七、解决跨域问题
/*** 解决跨域问题* @author smile*/
@Configuration
public class CorsConfig {@Beanpublic CorsFilter corsFilter() {final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();final CorsConfiguration corsConfiguration = new CorsConfiguration();/*是否允许请求带有验证信息*/corsConfiguration.setAllowCredentials(true);/*允许访问的客户端域名*/corsConfiguration.addAllowedOrigin("*");/*允许服务端访问的客户端请求头*/corsConfiguration.addAllowedHeader("*");/*允许访问的方法名,GET POST等*/corsConfiguration.addAllowedMethod("*");urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);return new CorsFilter(urlBasedCorsConfigurationSource);}
}
八、AOP统计接口访问次数
详见实战代码(四):Springboot AOP实现接口访问次数统计