实战代码(八):Springboot接口处理方法集合

一、理论基础

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 依赖引入



    com.github.xiaoymin
    knife4j-spring-boot-starter
    2.0.7

3.2 配置文件

knife4j:
  # 开启增强配置
  enable: true
  # 开启Swagger的Basic认证功能,默认是false
  basic:
    enable: true
    username: smile
    password: 123456
  # 配置文档路径
  documents:
    -
      group: 0.1
      name: 说明文档
      # 某一个文件夹下所有的.md文件
      locations: classpath:markdown/*
    -
      group: 0.1
      name: 说明文档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 依赖引入



    com.auth0
    java-jwt
    3.10.3

4.2 配置文件

my:
  security:
    username: api
    password: 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 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;
    }

    @Override
    public 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;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }


    @Override
    public 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;
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userTokenInterceptor)
                // 配置需要被拦截的接口
                // 注意此处是controller中的路径,配置文件中的server.servlet.context-path不能在此处加上,否则拦截器不会生效
                .addPathPatterns("/v0.1/**")
                // 不被拦截的接口
                .excludePathPatterns("/token/get")
        ;
    }
}

五、Validator参数校验

5.1 依赖引入



    org.hibernate.validator
    hibernate-validator

5.2 快速失败配置(可选)

/**
 * 参数校验快速失败配置
 * - 此模式下只要发现一个参数不匹配便会快速返回错误
 * @author smile
 */
@Configuration
public class ValidatorConfig {
    @Bean
    public 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 {
    @Bean
    public 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实现接口访问次数统计

你可能感兴趣的:(实战代码(八):Springboot接口处理方法集合)