實戰(zhàn)代碼(八):Springboot接口處理方法集合

一、理論基礎(chǔ)

1.1 如何實現(xiàn)一個相對健壯的接口

接口設計應該假設所有的調(diào)用者都是不靠譜的,所以需要做全方位的防御措施并盡可能考慮到各種因素

正常訪問

一個接口能正常訪問是最基本的、最低的要求。不管調(diào)用者傳遞什么參數(shù),接口應該都能給予良好的反饋,即使參數(shù)是錯的。

當用戶參數(shù)傳遞錯誤時,應該將錯誤信息反饋給用戶,比如缺少參數(shù)或者參數(shù)格式不正確等

返回值統(tǒng)一化

標準化的返回格式,是絕對有利于同事間的感情發(fā)展的。
如果你一會返回個S,一會返回個B,你一定會被詛咒成這個返回值的拼接體的(人工狗頭)

返回該返回的

盡可能的精簡返回值,不要返回過多的冗余信息,比如調(diào)用方只需要兩個字段,如果返回幾十個字段,對于調(diào)用者來講使用起來也不是很方便,而且還可能會暴露內(nèi)部的業(yè)務邏輯。

報錯信息全局處理

最常見的一個場景就是接口調(diào)用錯誤會將內(nèi)部代碼返給調(diào)用者,遇到過一個報名頁面報錯后,直接返回了幾行代碼,而且因為報錯的很多都是關(guān)鍵邏輯,連代碼注釋的作者都能看到。

所以未知的錯誤最好統(tǒng)一處理下,比如“網(wǎng)絡錯誤“等

及時更新的文檔

寫文檔是最不受待見的一件事,那么一個自動化接口文檔就很必要了,Swagger雖然繁瑣,但是用起來很香。

而且可以設置默認的一些參數(shù)值,直接調(diào)試接口,真的香。

版本管理

不管會不會迭代,最好加個版本號。一來可以讓接口看起來像”正規(guī)軍”,二來可以有效應對隨時到來的需求變更。

健壯的業(yè)務邏輯

這個就不多說了,相信每個人都有自己的一套方法論。

1.2 本實例集成的功能

  • 接口異常全局處理
  • Knife4j接口文檔
  • 簡單的token驗證
  • Validator參數(shù)校驗
  • 返回值的統(tǒng)一化
  • 解決跨域問題
  • AOP統(tǒng)計接口訪問次數(shù)

1.3 示例源碼地址

https://github.com/lysmile/spring-boot-demo/tree/master/spring-boot-api-handler-demo

二、接口異常全局處理

/**
 * 接口訪問統(tǒng)一異常處理
 * - 當程序報錯時,由此類進行統(tǒng)一處理
 * - 防止將程序內(nèi)部錯誤暴露給用戶
 * @author smile
 */
@RestControllerAdvice
@Slf4j
public class ControllerExceptionHandleAdvice {

    @ExceptionHandler(ValidatorRuntimeException.class)
    public CodeResult validationExceptionHandler(HttpServletRequest request, ValidatorRuntimeException e) {
        if (log.isDebugEnabled()) {
            log.error("接口[{}]請求發(fā)生[接口檢驗異常],錯誤信息:{}", 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("接口[{}]請求發(fā)生異常,錯誤信息:{}", 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("接口[{}]請求發(fā)生[token驗證異常],錯誤信息:{}, token:{}", request.getRequestURI(), e.getMessage(), request.getHeader("token"));
        }
        return new CodeResult(ResponseEnum.TOKEN_ERROR.getCode(), e.getMessage());
    }
}

三、Knife4j接口文檔

官網(wǎng)文檔: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認證功能,默認是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("!!成功進入接口, 參數(shù)是:[{}]", name);
        return new CodeResult(ResponseEnum.SUCCESS, name);
    }
}

完整代碼可參考本實例源碼

四、簡單的token驗證

本示例只實現(xiàn)了簡單的token驗證(驗證用戶和token是否過期)

優(yōu)化方向

  • Springboot全家桶:集成Springboot Security
  • 自實現(xiàn)功能:Jwt + Redis可實現(xiàn)較為完善的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: 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<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 攔截器功能實現(xiàn)

/**
 * 攔截器
 * - 驗證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參數(shù)校驗

5.1 依賴引入

<!-- 參數(shù)檢驗 -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
</dependency>

5.2 快速失敗配置(可選)

/**
 * 參數(shù)校驗快速失敗配置
 * - 此模式下只要發(fā)現(xiàn)一個參數(shù)不匹配便會快速返回錯誤
 * @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 工具類

/**
 * 參數(shù)驗證通用方法提取
 * @author smile
 */
public class ValidatorUtils {

    /**
     * controller bindingResult處理
     * @param bindingResult 參數(shù)驗證結(jié)果集
     */
    public static void handleBindingResult(BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            throw new ValidatorRuntimeException(bindingResult.getAllErrors().get(0).getDefaultMessage());
        }
    }
}

5.4 簡單應用

關(guān)鍵詞:@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())));
}

六、接口返回值統(tǒng)一化

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*:業(yè)務內(nèi)自定義錯誤碼
 * 2*:
 * 3*:
 * 4*:網(wǎng)絡錯誤
 * 5*:系統(tǒng)內(nèi)部錯誤,包含代碼執(zhí)行異常等
 * @author smile
 *
 */
public enum ResponseEnum {

    /**
     * 請求成功
     */
    SUCCESS("0", "success"),
    /**
     * 請求參數(shù)錯誤
     */
    REQUEST_PARAM_ERROR("1001", "參數(shù)錯誤"),
    /**
     * token驗證失敗
     */
    TOKEN_ERROR("1002", "token驗證失敗"),
    /**
     * 服務內(nèi)部異常,包括代碼執(zhí)行錯誤及一些不確定錯誤
     */
    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統(tǒng)計接口訪問次數(shù)

詳見實戰(zhàn)代碼(四):Springboot AOP實現(xiàn)接口訪問次數(shù)統(tǒng)計

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容