相关推荐recommended
引领位置服务驱动:腾讯地图 WebService 服务端 API 实用指南
作者:mmseoamin日期:2023-11-30

引领位置服务驱动:腾讯地图 WebService 服务端 API 实用指南,在这里插入图片描述,第1张

🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者

📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代

🌲文章所在专栏:业务设计

🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识

💬 向我询问任何您想要的东西,ID:vnjohn

🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏

😄 代词: vnjohn

⚡ 有趣的事实:音乐、跑步、电影、游戏

目录

  • 简介
  • 配置管理
    • 配额
    • 申请步骤
    • 地理位置解析
      • 基础代码配置
        • 腾讯 API 配置信息
        • 基础地图结构定义
        • 公用数据结构抽象
        • 请求工具类
        • IP 定位接口
        • 地址解析接口
        • 逆地址解析接口
        • 关键字输入提示接口
        • 总结

          简介

          在工作中,一旦涉及到地理位置经纬度的解析,通过经纬度解析所在的详细地址,通过 IP 解析所在的经纬度再解析所在的详细地址,常用的地图 API 有百度、腾讯等第三方可以用于集成在项目中使用,本文我们会以腾讯地图 API 实现地理位置信息解析,提供源码以及应用的场景

          腾讯地图 API 官方文档:WebService API|腾讯位置服务

          腾讯地图 WebService API 是基于 HTTPS/HTTP 协议的数据接口

          开发者可以使用任何客户端、服务器和开发语言,按照腾讯地图 WebService API 规范,按需构建 HTTPS 请求,并获取结果数据(目前支持JSON/JSONP方式返回)

          配置管理

          配额

          针对个人开发者和企业开发者,提供的服务 API 调用量有明显的差别,以下图来自官方文档

          引领位置服务驱动:腾讯地图 WebService 服务端 API 实用指南,在这里插入图片描述,第2张

          从上图可知,普通的接口 API 能够让我们自由的调用,根据不同的身份来区分对应的可用额度,个人开发者 -> 企业开发者 -> 商业授权开发者

          除了:路线规划|距离矩阵:货车|智能地址解析这些复杂的地理位置信息采集,其他啊的接口都有可限的使用额度

          申请步骤

          前往腾讯位置服务注册页,通过手机号+邮箱认证号个人开发者身份,随即登录至控制台

          1、创建应用

          引领位置服务驱动:腾讯地图 WebService 服务端 API 实用指南,在这里插入图片描述,第3张

          应用是最大的承载能力,一个应用下可以添加多个对应的 Key,而应用下所属的 Key 才是我们服务端调用 API 必须要的能力

          2、创建 Key

          引领位置服务驱动:腾讯地图 WebService 服务端 API 实用指南,在这里插入图片描述,第4张

          启用产品:WebServiceAPI、SDK、微信小程序

          WebServiceAPI 产品是用于服务端调用的能力,有三种可配置能力:域名白名单、授权 IP、签名检验

          1、域名白名单:当在调用腾讯地图 API 时传入当前所要服务的域名,请求的域名在域名白名单内,才能完成服务的请求

          2、授权 IP:当在调用腾讯地图 API 时传入当前所要请求的 IP 地址,请求的 IP 在「授权IP」内,才能完成服务的请求

          3、签名校验:当采用这种方式时,腾讯地图 API 会为我们分配一个 Secret key 值,同时会为我们提供具体的签名校验方式,让我们能够快速接入

          一般在工作中较多采用的是第一种、第二种方式,第一种、第二种没有那么大的复杂性不需要额外的技术成本去接入但安全性不高,第三种方式会增加接入的复杂性要额外的技术知识去实现但安全性较高

          WebServiceAPI 如何使用的官方文档:WebServiceAPI 配置

          「SDK」产品是用于安卓端、IOS 端使用的能力,当 App 应用需要使用到腾讯地图的地图、导航服务时,需要配置好该产品的能力,它的配置方式则与 WebServiceAPI 不同,它要配置的是安卓应用或 IOS 应用打包以后生成的包名称,可支持配置多个,每行配一个,多个进行隔行分开

          「微信小程序」产品是用于在微信小程序端使用的能力,当小程序内需要用到腾讯地图的某些功能时,例如:路线规划,那么使用它是非常好的选择

          3、当创建 Key 完成以后,会生成一个随机的密钥字符

          引领位置服务驱动:腾讯地图 WebService 服务端 API 实用指南,在这里插入图片描述,第5张

          4、点击详情,可查看具体分配的接口 API 调用额度信息

          引领位置服务驱动:腾讯地图 WebService 服务端 API 实用指南,在这里插入图片描述,第6张

          当您是个人开发者时,默认只会给你一个 Key 的可用额度,再申请其他的 Key 是无法获取额度的

          地理位置解析

          当应用和 Key 配置好以后,下面我们就通过官方文档 API 来进行以下几种 API 具体的实现

          基础代码配置

          腾讯 API 配置信息

          /**
           * 腾讯地图 API 配置信息
           *
           * @author vnjohn
           * @since 2023/10/25
           */
          @Component
          public class TencentMapConfig {
              @Value("${tencent.map.key}")
              private String tencentMapKey;
              protected static String PARSE_IP = "https://apis.map.qq.com/ws/location/v1/ip?key=%s&ip=%s";
              protected static String PARSE_ADDRESS = "https://apis.map.qq.com/ws/geocoder/v1/?key=%s&address=%s";
              protected static String PARSE_INVERSE_ADDRESS = "https://apis.map.qq.com/ws/geocoder/v1/?key=%s&location=%s,%s&get_poi=0";
              protected static String PLACE_TIPS = "https://apis.map.qq.com/ws/place/v1/suggestion?key=%s&keyword=%s&page_index=%s&page_size=%s&location=%s,%s";
          	// .......
          }
          

          调用 API 接口所需的 Key 信息以及其他 API 完整的 URL 地址,单独放在一个类进行存储

          一般我们会放在 Nacos 中进行存储,当发生 URL 参数变更时,可以结合 Nacos 动态刷新机制一起使用

          基础地图结构定义

          /**
           * @author vnjohn
           * @since 2023/7/23
           */
          @Data
          public class TencentMapResult {
              /**
               * 状态码,0为正常,其它为异常
               */
              private Integer status;
              /**
               * 状态说明
               */
              private String message;
              /**
               * 查询结果总数量
               */
              private Integer count;
              /**
               * 返回数据 > 数组
               */
              private T data;
              /**
               * 返回数据 > 对象
               */
              private T result;
          }
          

          因为在腾讯位置服务提供的 API 接口中,不同的接口可能它返回的结构不一样,有的返回是数组,有的返回是对象,所以在我们对接使用这一侧,就必须对不同的数据结构做一层适配,故而之采用泛型的结构来接收处理

          公用数据结构抽象

          /**
           * 腾讯地图「经纬度」
           *
           * @author vnjohn
           * @since 2023/7/23
           */
          @Data
          public class TencentLocation {
              /**
               * 经度
               */
              private Double lng;
              /**
               * 纬度
               */
              private Double lat;
          }
          

          在接收 API 接口返回的结果时,这一块的结构可以在多个地方同时运用到,故将它抽象出来作为一个单独的类

          请求工具类

          在简介中介绍到了「腾讯地图 WebService API 是基于 HTTPS/HTTP 协议的数据接口」那么我们就调用这部分数据接口时,就需要一个 HTTPS、HTTP 工具来进行调用,从而拿到具体的返回结果,在这里我们采用了 Spring 提供的客户端请求类:org.springframework.web.client.RestTemplate,如下的源码就是基于它进行再次封装,我们只需要关心具体的结果即可,在再次封装中我会适配好 HTTP、HTTPS 协议实现,如下:

          /**
           * 远程调用 URL 工具类
           *
           * @author vnjohn
           * @date 2022/1/7
           */
          @Slf4j
          @Configuration
          public class RestTemplateUtils {
              private static final RestTemplate restTemplate = new RestTemplate();
              @PostConstruct
              public void initial() {
                  restTemplate.getMessageConverters().add(new WxHttpMessageConverter());
                  restTemplate.setRequestFactory(clientHttpRequestFactory());
                  restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
              }
              /**
               * 添加对 HTTPS 的支持.
               *
               * @return
               */
              @Bean
              public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
                  try {
                      HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
                      SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null,
                              (TrustStrategy) (arg0, arg1) -> true).build();
                      httpClientBuilder.setSSLContext(sslContext);
                      HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
                      SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext,
                              hostnameVerifier);
                      Registry socketFactoryRegistry = RegistryBuilder.create()
                                                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                                                .register("https", sslConnectionSocketFactory).build();
                      PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(
                              socketFactoryRegistry);
                      poolingHttpClientConnectionManager.setMaxTotal(2700);
                      poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100);
                      httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
                      httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true));
                      HttpClient httpClient = httpClientBuilder.build();
                      HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(
                              httpClient);
                      clientHttpRequestFactory.setConnectTimeout(20000);
                      clientHttpRequestFactory.setReadTimeout(30000);
                      clientHttpRequestFactory.setConnectionRequestTimeout(20000);
                      return clientHttpRequestFactory;
                  } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
                      log.error("===> init http client pool error!!!", e);
                  }
                  return null;
              }
              // ----------------------------------GET-------------------------------------------------------
              /**
               * GET请求调用方式
               *
               * @param url          请求URL
               * @param responseType 返回对象类型
               * @return ResponseEntity 响应对象封装类
               */
              public static  ResponseEntity get(String url, Class responseType) {
                  return restTemplate.getForEntity(url, responseType);
              }
              /**
               * GET请求调用方式
               *
               * @param url          请求URL
               * @param responseType 返回对象类型
               * @param uriVariables URL中的变量,按顺序依次对应
               * @return ResponseEntity 响应对象封装类
               */
              public static  ResponseEntity get(String url, Class responseType, Object... uriVariables) {
                  return restTemplate.getForEntity(url, responseType, uriVariables);
              }
              /**
               * GET请求调用方式
               *
               * @param url          请求URL
               * @param responseType 返回对象类型
               * @param uriVariables URL中的变量,与Map中的key对应
               * @return ResponseEntity 响应对象封装类
               */
              public static  ResponseEntity get(String url, Class responseType, Map uriVariables) {
                  return restTemplate.getForEntity(url, responseType, uriVariables);
              }
              /**
               * 带请求头的GET请求调用方式
               *
               * @param url          请求URL
               * @param headers      请求头参数
               * @param responseType 返回对象类型
               * @param uriVariables URL中的变量,按顺序依次对应
               * @return ResponseEntity 响应对象封装类
               */
              public static  ResponseEntity get(String url, Map headers, Class responseType, Object... uriVariables) {
                  HttpHeaders httpHeaders = new HttpHeaders();
                  httpHeaders.setAll(headers);
                  return get(url, httpHeaders, responseType, uriVariables);
              }
              /**
               * 带请求头的GET请求调用方式
               *
               * @param url          请求URL
               * @param headers      请求头参数
               * @param responseType 返回对象类型
               * @param uriVariables URL中的变量,按顺序依次对应
               * @return ResponseEntity 响应对象封装类
               */
              public static  ResponseEntity get(String url, HttpHeaders headers, Class responseType, Object... uriVariables) {
                  HttpEntity requestEntity = new HttpEntity<>(headers);
                  return exchange(url, HttpMethod.GET, requestEntity, responseType, uriVariables);
              }
              /**
               * 带请求头的GET请求调用方式
               *
               * @param url          请求URL
               * @param headers      请求头参数
               * @param responseType 返回对象类型
               * @param uriVariables URL中的变量,与Map中的key对应
               * @return ResponseEntity 响应对象封装类
               */
              public static  ResponseEntity get(String url, Map headers, Class responseType, Map uriVariables) {
                  HttpHeaders httpHeaders = new HttpHeaders();
                  httpHeaders.setAll(headers);
                  return get(url, httpHeaders, responseType, uriVariables);
              }
              /**
               * 带请求头的GET请求调用方式
               *
               * @param url          请求URL
               * @param headers      请求头参数
               * @param responseType 返回对象类型
               * @param uriVariables URL中的变量,与Map中的key对应
               * @return ResponseEntity 响应对象封装类
               */
              public static  ResponseEntity get(String url, HttpHeaders headers, Class responseType, Map uriVariables) {
                  HttpEntity requestEntity = new HttpEntity<>(headers);
                  return exchange(url, HttpMethod.GET, requestEntity, responseType, uriVariables);
              }
              // ----------------------------------POST-------------------------------------------------------
              /**
               * POST请求调用方式
               *
               * @param url          请求URL
               * @param responseType 返回对象类型
               * @return
               */
              public static  ResponseEntity post(String url, Class responseType) {
                  return restTemplate.postForEntity(url, HttpEntity.EMPTY, responseType);
              }
          	// ......................
          }
          

          若想要关于该工具类的全部源码及依赖,可以私信博主或底下留言进行获取~

          IP 定位接口

          IP 定位,官方文档介绍:IP 定位

          IP 定位返回结构如下:

          /**
           * @author vnjohn
           * @since 2023/7/23
           */
          @Data
          public class TencentParseIpResponse {
              @ApiModelProperty("ip 信息")
              private String ip;
              @ApiModelProperty("所在定位")
              private TencentLocation location;
              @JsonProperty("ad_info")
              @ApiModelProperty("定位行政区划信息")
              private AdInfoComponent adInfoComponent;
              @Data
              public static class AdInfoComponent {
                  @ApiModelProperty("国家")
                  private String nation;
                  @ApiModelProperty("省")
                  private String province;
                  @ApiModelProperty("市")
                  private String city;
                  @ApiModelProperty("区")
                  private String district;
              }
          }
          

          IP 定位接口如何调用的方法如下:

          /**
           * 通过传入的 IP 解析所在经纬度「IP定位」
           *
           * @param ip IP
           * @return 解析地址后所得信息
           */
          public TencentMapResult parseIp(String ip) {
              TencentMapResult tencentMapResult = new TencentMapResult<>();
              // 拼接请求参数
              String parseIpRequestUrl = String.format(TencentMapConfig.PARSE_IP, tencentMapKey, ip);
              ResponseEntity parseIpResult = RestTemplateUtils.get(parseIpRequestUrl, TencentMapResult.class);
              // RestTemplate 请求未处理成功
              if (parseIpResult.getStatusCodeValue() != CommonConstant.EXTERNAL_SUCCESS_CODE || Objects.isNull(parseIpResult.getBody())) {
                  log.error("RestTemplate 请求腾讯地图 parseIpResult 失败,{}", JsonUtils.objToJsonStr(parseIpResult.getBody()));
                  return tencentMapResult;
              }
              if (!parseIpResult.getBody().getStatus().equals(CommonConstant.ZERO)) {
                  log.error("调用腾讯地图 parseIpResult API 失败,{}", JsonUtils.objToJsonStr(parseIpResult.getBody()));
                  return tencentMapResult;
              }
              Object data = parseIpResult.getBody().getResult();
              BeanUtils.copyProperties(parseIpResult.getBody(), tencentMapResult);
              TencentParseIpResponse parseIpResponse = JsonUtils.jsonStrToObj(JsonUtils.objToJsonStr(data), TencentParseIpResponse.class);
              tencentMapResult.setResult(parseIpResponse);
              return tencentMapResult;
          }
          

          地址解析接口

          地址解析,官方文档介绍:地址解析

          地址解析返回结构如下:

          /**
           * @author vnjohn
           * @since 2023/7/23
           */
          @Data
          public class TencentParseAddressResponse {
              @ApiModelProperty("提示文字")
              private String title;
              @ApiModelProperty("所在定位")
              private TencentLocation location;
              @JsonProperty("address_components")
              private AddressComponent addressComponent;
              @Data
              public static class AddressComponent {
                  @ApiModelProperty("省")
                  private String province;
                  @ApiModelProperty("市")
                  private String city;
                  @ApiModelProperty("区")
                  private String district;
                  @ApiModelProperty("街道")
                  private String street;
              }
          }
          

          地址解析接口如何调用|处理的方法如下:

          /**
           * 通过传入的地址解析所在经纬度「地址解析」
           *
           * @param address 地址信息
           * @return 解析地址后所得信息
           */
          public TencentMapResult parseAddress(String address) {
              TencentMapResult tencentMapResult = new TencentMapResult<>();
              // 拼接请求参数
              String parseAddressRequestUrl = String.format(TencentMapConfig.PARSE_ADDRESS, tencentMapKey, address);
              ResponseEntity parseAddressResult = RestTemplateUtils.get(parseAddressRequestUrl, TencentMapResult.class);
              // RestTemplate 请求未处理成功
              if (parseAddressResult.getStatusCodeValue() != CommonConstant.EXTERNAL_SUCCESS_CODE || Objects.isNull(parseAddressResult.getBody())) {
                  log.error("RestTemplate 请求腾讯地图 parseAddressResult 失败,{}", JsonUtils.objToJsonStr(parseAddressResult.getBody()));
                  return tencentMapResult;
              }
              if (!parseAddressResult.getBody().getStatus().equals(CommonConstant.ZERO)) {
                  log.error("调用腾讯地图 parseAddressResult API 失败,{}", JsonUtils.objToJsonStr(parseAddressResult.getBody()));
                  return tencentMapResult;
              }
              Object data = parseAddressResult.getBody().getResult();
              BeanUtils.copyProperties(parseAddressResult.getBody(), tencentMapResult);
              TencentParseAddressResponse parseAddressResponse = JsonUtils.jsonStrToObj(JsonUtils.objToJsonStr(data), TencentParseAddressResponse.class);
              tencentMapResult.setResult(parseAddressResponse);
              return tencentMapResult;
          }
          

          逆地址解析接口

          逆地址解析,官方文档介绍:地址解析

          逆地址解析返回结构如下:

          /**
           * @author vnjohn
           * @since 2023/7/23
           */
          @Data
          public class TencentParseInverseAddressResponse {
              @ApiModelProperty("详细地址信息")
              private String address;
              @ApiModelProperty("所在定位")
              private TencentLocation location;
              @JsonProperty("address_components")
              private AddressComponent addressComponent;
              @JsonProperty("formatted_addresses")
              private FormattedAddress formattedAddress;
              @Data
              public static class FormattedAddress {
                  @ApiModelProperty("推荐使用的地址描述,描述精确性较高")
                  private String recommend;
                  @ApiModelProperty("粗略位置描述")
                  private String rough;
              }
              @Data
              public static class AddressComponent {
                  @ApiModelProperty("省")
                  private String province;
                  @ApiModelProperty("市")
                  private String city;
                  @ApiModelProperty("区")
                  private String district;
                  @ApiModelProperty("街道")
                  private String street;
              }
          }
          

          逆地址解析接口如何调用|处理的方法如下:

          /**
           * 通过经纬度解析当前所在地址信息「逆地址解析」
           *
           * @param lng 经度
           * @param lat 纬度
           * @return 解析好的地址信息
           */
          public TencentMapResult parseInverseAddress(Double lng, Double lat) {
              TencentMapResult tencentMapResult = new TencentMapResult<>();
              // 拼接请求参数
              String parseInverseAddressRequestUrl = String.format(TencentMapConfig.PARSE_INVERSE_ADDRESS, tencentMapKey, lat, lng);
              ResponseEntity parseInverseAddressResult = RestTemplateUtils.get(parseInverseAddressRequestUrl, TencentMapResult.class);
              // RestTemplate 请求未处理成功
              if (parseInverseAddressResult.getStatusCodeValue() != CommonConstant.EXTERNAL_SUCCESS_CODE || Objects.isNull(parseInverseAddressResult.getBody())) {
                  log.error("RestTemplate 请求腾讯地图 parseInverseAddress 失败,{}", JsonUtils.objToJsonStr(parseInverseAddressResult.getBody()));
                  return tencentMapResult;
              }
              if (!parseInverseAddressResult.getBody().getStatus().equals(CommonConstant.ZERO)) {
                  log.error("调用腾讯地图 parseInverseAddress API 失败,{}", JsonUtils.objToJsonStr(parseInverseAddressResult.getBody()));
                  return tencentMapResult;
              }
              Object data = parseInverseAddressResult.getBody().getResult();
              BeanUtils.copyProperties(parseInverseAddressResult.getBody(), tencentMapResult);
              TencentParseInverseAddressResponse parseInverseAddressResponse = JsonUtils.jsonStrToObj(JsonUtils.objToJsonStr(data), TencentParseInverseAddressResponse.class);
              tencentMapResult.setResult(parseInverseAddressResponse);
              return tencentMapResult;
          }
          

          关键字输入提示接口

          关键字输入提示,官方文档介绍:关键字输入提示

          关键字输入提示返回结构如下:

          /**
           * 关键字输入提示响应内容
           *
           * @author vnjohn
           * @since 2023/7/23
           */
          @Data
          public class TencentPlaceTipsResponse implements Serializable {
              private static final long serialVersionUID = -3155685021522526885L;
              @ApiModelProperty("提示文字")
              private String title;
              @ApiModelProperty("地址")
              private String address;
              @ApiModelProperty("省")
              private String province;
              @ApiModelProperty("市")
              private String city;
              @ApiModelProperty("区")
              private String district;
              @ApiModelProperty("所在定位")
              private TencentLocation location;
          }
          

          关键字输入提示接口如何调用|处理的方法如下:

          /**
           * 关键字输入提示,获取可见提示内容
           *
           * @param keyword 关键字
           * @param page    页码
           * @param size    条数
           */
          public TencentMapResult> placeTips(String keyword, Double lng, Double lat, Integer page, Integer size) {
              TencentMapResult> tencentMapResult = new TencentMapResult<>();
              // 拼接请求参数
              String placeTipsRequestUrl = String.format(TencentMapConfig.PLACE_TIPS, tencentMapKey, keyword, page, size, lat, lng);
              ResponseEntity placeTipsResult = RestTemplateUtils.get(placeTipsRequestUrl, TencentMapResult.class);
              // RestTemplate 请求未处理成功
              if (placeTipsResult.getStatusCodeValue() != CommonConstant.EXTERNAL_SUCCESS_CODE || Objects.isNull(placeTipsResult.getBody())) {
                  log.error("RestTemplate 请求腾讯地图 placeTips 失败,{}", JsonUtils.objToJsonStr(placeTipsResult.getBody()));
                  return tencentMapResult;
              }
              if (!placeTipsResult.getBody().getStatus().equals(CommonConstant.ZERO)) {
                  log.error("调用腾讯地图 placeTips API 失败,{}", JsonUtils.objToJsonStr(placeTipsResult.getBody()));
                  return tencentMapResult;
              }
              Object data = placeTipsResult.getBody().getData();
              BeanUtils.copyProperties(placeTipsResult.getBody(), tencentMapResult);
              List tencentPlaceTipsList = JsonUtils.jsonStrToList(JsonUtils.objToJsonStr(data), TencentPlaceTipsResponse.class);
              tencentMapResult.setData(tencentPlaceTipsList);
              return tencentMapResult;
          }
          

          总结

          该篇博文简单了介绍地理位置解析在我们实际开发中的应用,在配置管理中告知了如何一步步进行注册以及配置相关的产品信息,在地理位置解析中将博主自身运用地理位置服务时,所实现的部分相关源码告知大家如何运用封装及抽象的特性将你的代码构建的更好更优,最后,通过实际工作中经常会运用到四种接口来作了一层层的剥离及代码实战,希望能够得到你的支持+喜欢,再次感谢您能支持此文章!

          🌟🌟🌟愿你我都能够在寒冬中相互取暖,互相成长,只有不断积累、沉淀自己,后面有机会自然能破冰而行!

          博文放在 业务设计 专栏里,欢迎订阅,会持续更新!

          如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

          推荐专栏:Spring、MySQL,订阅一波不再迷路

          大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!