百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

4、springboot脚手架-常用web配置

toyiye 2024-06-21 12:18 19 浏览 0 评论

1、一个单纯的springboot web工程

大家都知道springboot开箱即用的特性,一个springboot创建起来之后,不用做任何配置,只写一个启动类就可以,最多再加一个controller定义个接口,像这样:

@RestController
@RequestMapping("/api/any")
public class AnyController {

    @GetMapping("/list")
    public Map list() {
        Map ret = new HashMap();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        ret.put("time", sdf.format(new Date()));
        return ret;
    }
}

启动之后接口就可以访问了,但是,真有这么简单吗?前端同事对你这个单纯的接口能够满意吗?

前端同事跟我说,她比较关心以下几个问题:

  • 1、接口有统一返回吗?返回的字段是code-data,还是status-resp,还是error-code-or-data?
  • 2、除了正常情况的返回,异常情况但非500,是怎么返回的?http status code也会是除了200之外的值吗?
  • 3、传参是时间的话,是传毫秒吗?
  • 4、返回是时间的话,是返回毫秒吗?
  • 5、一个参数传空或者不传,结果是一样的吗?
  • 6、后端能给跨一下域吗?
  • 7、双人联调时你能快速从日志里找到我传的什么参吗?难道还要人家自己F12吗?
  • 8、要是有个long字段,是返回string还是怎么?考虑丢失精度问题了吗?
  • 9、你接口文档怎么给我?是swagger吗?
  • 10、分页的参数和返回是统一的吗?
  • 11、如果返回错误信息,后端会直接返回国际化的文本吗?还是要前端自己控制国际化?

我说,不要着急,你说的问题,确实存在,是我冒失了,这周就加班着重解决这些问题,除了国际化的问题,这个以后再说。

2、统一返回规格

在commons-model里,我们定义了一个AnyResponse,现在就是要让所有接口都返回这个格式,就算方法的返回值不是AnyResponse也要强行封装成AnyResponse返回。


package org.danger.dy.common.web.response;

import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;

import com.danger.dy.common.models.response.AnyResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.AbstractMappingJacksonResponseBodyAdvice;


/**
 * 统一接口返回规格
 */
@Slf4j
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
public class RestResponseWrapAdvice extends AbstractMappingJacksonResponseBodyAdvice {

    private AtomicInteger responseCount = new AtomicInteger(0);

    @Override
    protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer,
                                           MediaType contentType,
                                           MethodParameter returnType,
                                           ServerHttpRequest request,
                                           ServerHttpResponse response) {
        Method method = returnType.getMethod();
        if (method != null && MediaType.APPLICATION_JSON.equals(contentType)) {
            Object returnValue = bodyContainer.getValue();
            if (method.getReturnType() == ResponseEntity.class) {
                log.info("接口返回ResponseEntity,不进行任何修改");
                return;
            }
            if (returnValue instanceof AnyResponse) {
                response.setStatusCode(HttpStatus.valueOf(((AnyResponse) returnValue).getCode()));
            } else {
                bodyContainer.setValue(AnyResponse.ok(returnValue));
            }
            response.getHeaders().add("x-response-sequence", responseCount.incrementAndGet() + "");
        }
    }

}

逻辑是不是一目了然

  • 如果是404,500,spring自己会返回ResponseEntity,就不用拦截了。
  • 如果接口方法直接返回AnyResponse,那就将http status code设置为AnyResponse的code字段。
  • 如果接口方法返回的是随意的对象,则包装为AnyResponse,保持http status code为默认200。
  • 在响应中加一个header:x-response-sequence,没什么大用,暂时只是做个标记证明响应被拦截过。


3、统一异常处理

有了上面的统一响应之后,接口方法可以直接返回AnyErrorResponse,但接口调用各种service各种组件时,总不能期待所有方法都返回AnyResponse吧,肯定是要抛出异常的,我们在commons-model里定义了LogicException,但当真抛出LogicException时,我们还没有做任何处理,接口响应的只能是500,下面就要对异常拦截一下。

package org.danger.dy.common.web.error;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.danger.dy.common.models.exceptions.LogicException;
import com.danger.dy.common.models.response.AnyErrorResponse;
import com.danger.dy.common.models.response.AnyResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;


/**
 * 接口异常统一拦截
 */
@Slf4j
@RestControllerAdvice
public class ResponseExceptionHandler
        extends ResponseEntityExceptionHandler
        implements PriorityOrdered, Filter {

    private final ObjectMapper objectMapper;

    @Autowired
    public ResponseExceptionHandler(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }


    @ExceptionHandler(LogicException.class)
    protected AnyResponse onBizException(LogicException bizException,
                                         HttpServletRequest request) {
        // 将异常包装为AnyResponse
        AnyErrorResponse ret = AnyResponse.error(bizException.getStatusCode(), bizException.getLabel());
        return ret;
    }

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body,
                                                             HttpHeaders headers,
                                                             HttpStatus status, WebRequest request) {
        AnyErrorResponse<?> ret = AnyResponse.error(status.value(), status.toString());
        return ResponseEntity.status(status.value()).body(ret);
    }

    /**
     * 非可预期的异常,也包装成AnyResponse
     */
    @ExceptionHandler(Exception.class)
    protected AnyResponse onException(Exception exception, HttpServletRequest request) {
        log.error("Unexpected exception: ", exception);
        AnyErrorResponse ret = AnyResponse.error(500, ExceptionUtils.getMessage(exception));
        return ret;
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        try {
            filterChain.doFilter(servletRequest, servletResponse);
        } catch (LogicException bizException) {
            handleFilterBizException(bizException, servletRequest, servletResponse);
        }
    }

    /**
     * filter里抛出的异常处理
     */
    private void handleFilterBizException(LogicException bizException, ServletRequest servletRequest, ServletResponse servletResponse) {
        try {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            AnyResponse<?> errorResponse = onBizException(bizException, request);
            response.setStatus(errorResponse.getCode());
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
        } catch (Exception e) {
            log.error("logic error: ", bizException);
            log.error("response error: ", e);
        }
    }
}

4、我们再来写一个接口试试


@RestController
@RequestMapping("/api/any")
public class AnyController {

    @GetMapping("/test")
    public AnyResponse<AnyObject> test(@RequestParam(name = "want") String want) {
        if (want.equals("500")) {
            throw new RuntimeException("who's your daddy");
        } else if (want.equals("406")) {
            throw new LogicException(406, "greed is good");
        } else if (want.equals("error_response")) {
            return AnyResponse.error(406, "the mountain is high");
        } else {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
            return AnyResponse.ok(AnyObject.of(sdf.format(new Date())));
        }
    }
}

请求1:不带任何参数,返回

HTTP/1.1 400 

{
  "code": 400,
  "message": "400 BAD_REQUEST"
}

请求2:抛出RuntimeException,返回

HTTP/1.1 500 

{
  "code": 500,
  "message": "RuntimeException: who's your daddy"
}

请求3:抛出LogicException,返回

HTTP/1.1 406 

{
  "code": 406,
  "message": "greed is good"
}

请求4:抛出LogicException,返回

HTTP/1.1 406 

{
  "code": 406,
  "message": "the mountain is high"
}

请求5:正常

HTTP/1.1 200 

{
  "code": 200,
  "data": {
    "result": "2023-04-15 23:08:04.952"
  }
}

基本能够实现返回与异常的统一处理。

5、接口请求日志

记录日志的重要性应该不需要多说了,作为一个程序员,没日志不行,日志多了也不行,程序员与日志是相辅相成相爱相杀,日志怎么查,在这里不重要,首先要确定的事:你记录了日志。

在我这些年无数次的经过同事身后,有那么三两次,我看到同事在查:spring打印接口请求参数。

这一节就讲这个,打印请求参数当然主要是为了排查,但参数有时可能不在get和post参数里,而是在request body里,即需要去读http请求的输入流,而流默认情况下又只能读一次,那首先就是要解决让流可以重复读取的问题。

#保证请求的InputStream可以被重复读取

基本思路就是把原始body读出来,保存下来,再对外提供一个可以反复读取的InputStream,看代码:

package org.danger.dy.common.web.logging;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.MediaType;

@Slf4j
public class DyRepeatableFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if ((request instanceof HttpServletRequest)
                && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
            requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
        }

        if (null == requestWrapper) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
    }

    @Override
    public void destroy() {

    }


    /**
     * 可重复读取inputStream的request
     */
    public static class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
        private final byte[] body;

        public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {
            super(request);
            request.setCharacterEncoding("UTF-8");
            response.setCharacterEncoding("UTF-8");

            body = getBodyString(request).getBytes("UTF-8");
        }

        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            final ByteArrayInputStream bais = new ByteArrayInputStream(body);
            return new ServletInputStream() {
                @Override
                public int read() throws IOException {
                    return bais.read();
                }

                @Override
                public int available() throws IOException {
                    return body.length;
                }

                @Override
                public boolean isFinished() {
                    return false;
                }

                @Override
                public boolean isReady() {
                    return false;
                }

                @Override
                public void setReadListener(ReadListener readListener) {

                }
            };
        }


        public static String getBodyString(ServletRequest request) {
            StringBuilder sb = new StringBuilder();
            BufferedReader reader = null;
            try (InputStream inputStream = request.getInputStream()) {
                reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
                String line = "";
                while ((line = reader.readLine()) != null) {
                    sb.append(line);
                }
            } catch (IOException e) {
                log.error("filter error", e);
                throw new RuntimeException(e);
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                        log.error(ExceptionUtils.getMessage(e));
                    }
                }
            }
            return sb.toString();
        }
    }

}

#打印请求日志

也是通过filter解决。

package org.danger.dy.common.web.logging;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.MediaType;

@Slf4j
public class DyRepeatableFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if ((request instanceof HttpServletRequest)
                && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
            requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
        }

        if (null == requestWrapper) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
    }

    @Override
    public void destroy() {

    }


    /**
     * 可重复读取inputStream的request
     */
    public static class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
        private final byte[] body;

        public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {
            super(request);
            request.setCharacterEncoding("UTF-8");
            response.setCharacterEncoding("UTF-8");

            body = getBodyString(request).getBytes("UTF-8");
        }

        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            final ByteArrayInputStream bais = new ByteArrayInputStream(body);
            return new ServletInputStream() {
                @Override
                public int read() throws IOException {
                    return bais.read();
                }

                @Override
                public int available() throws IOException {
                    return body.length;
                }

                @Override
                public boolean isFinished() {
                    return false;
                }

                @Override
                public boolean isReady() {
                    return false;
                }

                @Override
                public void setReadListener(ReadListener readListener) {

                }
            };
        }


        public static String getBodyString(ServletRequest request) {
            StringBuilder sb = new StringBuilder();
            BufferedReader reader = null;
            try (InputStream inputStream = request.getInputStream()) {
                reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
                String line = "";
                while ((line = reader.readLine()) != null) {
                    sb.append(line);
                }
            } catch (IOException e) {
                log.error("filter error", e);
                throw new RuntimeException(e);
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                        log.error(ExceptionUtils.getMessage(e));
                    }
                }
            }
            return sb.toString();
        }
    }

}

加载filter

上面定义的两个filter并非可以被自动扫描并加载的Bean,需要配置:


@Configuration
@ComponentScan
@AutoConfigureBefore({WebMvcAutoConfiguration.class})
public class DyWebAutoConfiguration {

    @Bean(name = "loggingFilter")
    public FilterRegistrationBean<DyLoggingFilter> loggingFilterRegisterBean() {
        FilterRegistrationBean<DyLoggingFilter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new DyLoggingFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.setName("loggingFilter");
        filterRegistrationBean.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 1); //order的数值越小 则优先级越高
        return filterRegistrationBean;
    }

    @Bean(name = "repeatableFilter")
    public FilterRegistrationBean<DyRepeatableFilter> repeatableFilter() {
        FilterRegistrationBean<DyRepeatableFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new DyRepeatableFilter());
        registration.addUrlPatterns("/*");
        registration.setName("repeatableFilter");
        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
        return registration;
    }
}

看效果

再发起请求时,请求日志打印出来是这样:

>>>[request finished: /api/any/test, from 127.0.0.1](耗时5ms) params={"want":"406dd"}, body=null, response status=200

上面留了一点儿瑕疵,就是对日志行为进行控制,控制的方式就是在yml文件里进行配置,这部分内容我们打算放到最后对dy lib进行统一配置时再考虑,这里不做过多深究。

6、与前端沟通

谢谢同事的肯定,我一定会继续努力,争取获得与小姐姐一块加班的资格。

7、总结

今天就到这里了,对于前端提出的问题,我们并不打算一次性进行解决,编程讲究迭代、渐进、细水长流,只有这样,才能不断精进,并通过与同事的反复沟通,将细节打磨到极致。


相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码