2023年4月

分析查询语句:EXPLAIN


1概述


定位了查询慢的SQL之后,就可以使用EXPLAIN或者DESCRIBE工具做针对性的分析查询
。两者使用方法相同,并且分析结果也是相同的。

​ MySQL中有专门负责SQL语句优化的优化器模块,主要功能是计算分析系统中收集到的统计信息,为客户端请求的Query提供它最优的
执行计划

它认为的最优数据检索方案毕竟是自动分析成的,所以不一定是DBA或者开发人员认为的最优方案

​ 这个执行计划展示了接下来进行具体查询的方式,比如
多表连接的顺序

对每个表采用什么方式进行具体的查询
等等,MySQL提供的EXPLAIN语句可以用来查询某个查询语句的具体执行计划,根据EXPLAIN语句的输出项,可以有针对性地提升查询SQL的性能。

能查到什么?
  • 表的读取顺序
  • 数据读取操作的操作类型
  • 哪些索引可以被使用
  • 哪些索引实际被使用
  • 表之间的引用关系
  • 每张表有多少行被优化器查询
版本区别
  • MySQL5.6.3之前只能使用
    EXPLAIN SELECT
    ,之后可以使用
    EXPLAIN SELECT, UPDATE, DELETE
  • 5.7之前的版本,想要查看
    partitions(分区)

    filtered
    需要使用
    EXPLAIN partitions、filtered
    ,而5.7之后直接默认显示
数据准备
创建表
CREATE TABLE s1 (
	id INT AUTO_INCREMENT,
	key1 VARCHAR(100),
	key2 INT,
	key3 VARCHAR(100),
	key_part1 VARCHAR(100),
	key_part2 VARCHAR(100),
	key_part3 VARCHAR(100),
	common_field VARCHAR(100),
	PRIMARY KEY (id),
	INDEX idx_key1 (key1),
	UNIQUE INDEX idx_key2(key2),
	INDEX idx_key3(key3),
	INDEX idx_key_part(key_part1, key_part2, key_part3)
)ENGINE=INNODB CHARSET=utf8


CREATE TABLE s2 (
	id INT AUTO_INCREMENT,
	key1 VARCHAR(100),
	key2 INT,
	key3 VARCHAR(100),
	key_part1 VARCHAR(100),
	key_part2 VARCHAR(100),
	key_part3 VARCHAR(100),
	common_field VARCHAR(100),
	PRIMARY KEY (id),
	INDEX idx_key1 (key1),
	UNIQUE INDEX idx_key2(key2),
	INDEX idx_key3(key3),
	INDEX idx_key_part(key_part1, key_part2, key_part3)
)ENGINE=INNODB CHARSET=utf8
创建存储函数
-- 函数返回随机字符串
DELIMITER //

CREATE FUNCTION `rand_string`(n INT) RETURNS varchar(255) CHARSET utf8mb4
BEGIN 
	DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
	DECLARE return_str VARCHAR(255) DEFAULT '';
	DECLARE i INT DEFAULT 0;
	WHILE i < n DO 
       SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
       SET i = i + 1;
    END WHILE;
    RETURN return_str;
END //
DELIMITER ;

首先要确保相信函数的变量
log_bin_trust_function_creators
为1

SELECT @@log_bin_trust_function_creators variable;

SET GLOBAL log_bin_trust_function_creators = 1;
存储过程

​ 向s1、s2表添加数据的存储过程

DELIMITER //
CREATE PROCEDURE insert_s1 (IN min_num INT (10), IN max_num INT(10))
BEGIN
	DECLARE i INT DEFAULT 0;
	SET autocommit = 0;
	REPEAT
	SET i = i + 1;
	INSERT INTO s1 VALUES(
		(min_num + i),
		rand_string(6),
		(min_num + 30* i + 5),
		rand_string(6),
		rand_string(10),
		rand_string(5),
		rand_string(10),
		rand_string(10)
	);
	UNTIL i = max_num
	END REPEAT;
	COMMIT;
END //
DELIMITER;



DELIMITER //
CREATE PROCEDURE insert_s2 (IN min_num INT (10), IN max_num INT(10))
BEGIN
	DECLARE i INT DEFAULT 0;
	SET autocommit = 0;
	REPEAT
	SET i = i + 1;
	INSERT INTO s1 VALUES(
		(min_num + i),
		rand_string(6),
		(min_num + 30* i + 5),
		rand_string(6),
		rand_string(10),
		rand_string(5),
		rand_string(10),
		rand_string(10)
	);
	UNTIL i = max_num
	END REPEAT;
	COMMIT;
END //
DELIMITER;
执行存储过程添加数据
CALL insert_s1(10001, 10000);
CALL insert_s2(10001, 10000);
Explain的输出列

image-20230413142058178

列名 描述
id 在一个大的查询语句中每个SELECT关键字都对应着一个唯一的id
select_type SELECT关键字对应查询的类型
table 表名
partitions 匹配的分区信息
type 针对单表的访问方法
possible_keys 可能使用到的索引
key 实际使用的索引
key_len 实际使用到的索引长度
ref 当使用索引列等值查询的时候,与索引列进行等值匹配的对象信息
rows 预估需要读取的记录条数
filtered 某个表经过搜索条件过滤后剩余记录条数的百分比
Extra 一些额外的信息
1 id

​ id,在一个
大的查询语句
中每个
SELECT关键字
都对应着一个唯一的id,所以有几个select关键字就会有几个id:

EXPLAIN SELECT * FROM s1

image-20230413152527504

EXPLAIN SELECT * FROM s1 INNER JOIN s2

image-20230413152550172

上面的两个SQL都只有一个select所以只有一个id

EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = 'a'

子查询有两个select,所以对应两个id1和2

image-20230413153710803

作者:京东零售 王鹏超

1.什么是参数解析器

@RequstBody、@RequstParam 这些注解是不是很熟悉?

我们在开发Controller接口时经常会用到此类参数注解,那这些注解的作用是什么?我们真的了解吗?

简单来说,这些注解就是帮我们将前端传递的参数直接解析成直接可以在代码逻辑中使用的javaBean,例如@RequstBody接收json参数,转换成java对象,如下所示:

前台传参 参数格式
application/json

正常代码书写如下:

@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestBody UserInfo userInfo){
    //***
    return userInfo.getName();
}



但如果是服务接收参数的方式改变了,如下代码,参数就不能成功接收了,这个是为什么呢?

@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestBody String userName, @RequestBody Integer userId){
    //***
    return userName;
}



如果上面的代码稍微改动一下注解的使用并且前台更改一下传参格式,就可以正常解析了。

前台传参 参数格式
http://***?userName=Alex&userId=1
@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestParam String userName, @RequestParam Integer userId){
    //***
    return userName;
}



这些这里就不得不引出这些注解背后都对应的内容—Spring提供的参数解析器,这些参数解析器帮助我们解析前台传递过来的参数,绑定到我们定义的Controller入参上,不通类型格式的传递参数,需要不同的参数解析器,有时候一些特殊的参数格式,甚至需要我们自定义一个参数解析器。

不论是在SpringBoot还是在Spring MVC中,一个HTTP请求会被DispatcherServlet类接收(本质是一个Servlet,继承自HttpServlet)。Spring负责从HttpServlet中获取并解析请求,将请求uri匹配到Controller类方法,并解析参数并执行方法,最后处理返回值并渲染视图。

参数解析器的作用就是将http请求提交的参数转化为我们controller处理单元的入参
。原始的Servlet获取参数的方式如下,需要手动从HttpServletRequest中获取所需信息。

@WebServlet(urlPatterns="/getResource")
public class resourceServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        /**获取参数开始*/
        String resourceId = req.getParameter("resourceId");
        String resourceType = req.getHeader("resourceType");
        /**获取参数结束*/
        resp.setContentType("text/html;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.println("resourceId " + resourceId + " resourceType " + resourceType);
    }
}



Spring为了帮助开发者解放生产力,提供了一些特定格式(header中content-type对应的类型)入参的参数解析器,我们在接口参数上只要加上特定的注解(当然不加注解也有默认解析器),就可以直接获取到想要的参数,不需要我们自己去HttpServletRequest中手动获取原始入参,如下所示:

@RestController
public class resourceController {

  @RequestMapping("/resource")
  public String getResource(@RequestParam("resourceId") String resourceId,
            @RequestParam("resourceType") String resourceType,
            @RequestHeader("token") String token) {
    return "resourceId" + resourceId + " token " + token;
  }
}



常用的注解类参数解析器使用方式以及与注解的对应关系对应关系如下:

注解命名 放置位置 用途
@PathVariable 放置在参数前 允许request的参数在url路径中
@RequestParam 放置在参数前 允许request的参数直接连接在url地址后面,也是Spring默认的参数解析器
@RequestHeader 放置在参数前 从请求header中获取参数
@RequestBody 放置在参数前 允许request的参数在参数体中,而不是直接连接在地址后面
注解命名 对应的解析器 content-type
@PathVariable PathVariableMethodArgumentResolver
@RequestParam RequestParamMethodArgumentResolver 无(get请求)和multipart/form-data
@RequestBody RequestResponseBodyMethodProcessor application/json
@RequestPart RequestPartMethodArgumentResolver multipart/form-data

2.参数解析器原理

要了解参数解析器,首先要了解一下最原始的Spring MVC的执行过程。客户端用户发起一个Http请求后,请求会被提交到前端控制器(Dispatcher Servlet),由前端控制器请求处理器映射器(步骤1),处理器映射器会返回一个执行链(Handler Execution 步骤2),我们通常定义的拦截器就是在这个阶段执行的,之后前端控制器会将映射器返回的执行链中的Handler信息发送给适配器(Handler Adapter 步骤3),适配器会根据Handler找到并执行相应的Handler逻辑,也就是我们所定义的Controller控制单元(步骤4),Handler执行完毕会返回一个ModelAndView对象,后续再经过视图解析器解析和视图渲染就可以返回给客户端请求响应信息了。

在容器初始化的时候,RequestMappingHandlerMapping 映射器会将 @RequestMapping 注解注释的方法存储到缓存,其中key是 RequestMappingInfo,value是HandlerMethod。HandlerMethod 是如何进行方法的参数解析和绑定,就要了解请求参数适配器**RequestMappingHandlerAdapter,**该适配器对应接下来的参数解析及绑定过程。源码路径如下:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

RequestMappingHandlerAdapter
大致的解析和绑定流程如下图所示,

RequestMappingHandlerAdapter实现了接口InitializingBean,在Spring容器初始化Bean后,调用方法afterPropertiesSet( ),将默认参数解析器绑定HandlerMethodArgumentResolverComposite 适配器的参数 argumentResolvers上,其中HandlerMethodArgumentResolverComposite是接口HandlerMethodArgumentResolver的实现类。源码路径如下:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#afterPropertiesSet

@Override
public void afterPropertiesSet() {
   // Do this first, it may add ResponseBody advice beans
   initControllerAdviceCache();

   if (this.argumentResolvers == null) {
      /**  */
      List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
      this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
   }
   if (this.initBinderArgumentResolvers == null) {
      List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
      this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
   }
   if (this.returnValueHandlers == null) {
      List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
      this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
   }
}



通过getDefaultArgumentResolvers( )方法,可以看到Spring为我们提供了哪些默认的参数解析器,这些解析器都是
HandlerMethodArgumentResolver
接口的实现类。

针对不同的参数类型,Spring提供了一些基础的参数解析器,其中有基于注解的解析器,也有基于特定类型的解析器,当然也有兜底默认的解析器,如果已有的解析器不能满足解析要求,Spring也提供了支持用户自定义解析器的扩展点,源码如下:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getDefaultArgumentResolvers

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
   List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();

   // Annotation-based argument resolution 基于注解
   /** @RequestPart 文件注入 */
   resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
   /** @RequestParam 名称解析参数 */
   resolvers.add(new RequestParamMapMethodArgumentResolver());
   /** @PathVariable url路径参数 */
   resolvers.add(new PathVariableMethodArgumentResolver());
   /** @PathVariable url路径参数,返回一个map */
   resolvers.add(new PathVariableMapMethodArgumentResolver());
   /** @MatrixVariable url矩阵变量参数 */
   resolvers.add(new MatrixVariableMethodArgumentResolver());
   /** @MatrixVariable url矩阵变量参数 返回一个map*/
   resolvers.add(new Matrix VariableMapMethodArgumentResolver());
   /** 兜底处理@ModelAttribute注解和无注解 */
   resolvers.add(new ServletModelAttributeMethodProcessor(false));
   /** @RequestBody body体解析参数 */
   resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
   /** @RequestPart 使用类似RequestParam */
   resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
   /** @RequestHeader 解析请求header */
   resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
   /** @RequestHeader 解析请求header,返回map */
   resolvers.add(new RequestHeaderMapMethodArgumentResolver());
   /** Cookie中取值注入 */
   resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
   /** @Value */
   resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
   /** @SessionAttribute */
   resolvers.add(new SessionAttributeMethodArgumentResolver());
   /** @RequestAttribute */
   resolvers.add(new RequestAttributeMethodArgumentResolver());

   // Type-based argument resolution 基于类型
   /** Servlet api 对象 HttpServletRequest 对象绑定值 */
   resolvers.add(new ServletRequestMethodArgumentResolver());
   /** Servlet api 对象 HttpServletResponse 对象绑定值 */
   resolvers.add(new ServletResponseMethodArgumentResolver());
   /** http请求中 HttpEntity RequestEntity数据绑定 */
   resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
   /** 请求重定向 */
   resolvers.add(new RedirectAttributesMethodArgumentResolver());
   /** 返回Model对象 */
   resolvers.add(new ModelMethodProcessor());
   /** 处理入参,返回一个map */
   resolvers.add(new MapMethodProcessor());
   /** 处理错误方法参数,返回最后一个对象 */
   resolvers.add(new ErrorsMethodArgumentResolver());
   /** SessionStatus */
   resolvers.add(new SessionStatusMethodArgumentResolver());
   /**  */
   resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

   // Custom arguments 用户自定义
   if (getCustomArgumentResolvers() != null) {
      resolvers.addAll(getCustomArgumentResolvers());
   }

   // Catch-all 兜底默认
   resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
   resolvers.add(new ServletModelAttributeMethodProcessor(true));

   return resolvers;
}



HandlerMethodArgumentResolver
接口中只定义了两个方法,分别是解析器适用范围确定方法supportsParameter( )和参数解析方法resolveArgument(),不同用途的参数解析器的使用差异就体现在这两个方法上,这里就不具体展开参数的解析和绑定过程。

3.自定义参数解析器的设计

Spring的设计很好践行了开闭原则,不仅在封装整合了很多非常强大的能力,也为用户留好了自定义拓展的能力,参数解析器也是这样,Spring提供的参数解析器基本能满足常用的参数解析能力,但很多系统的参数传递并不规范,比如京东color网关传业务参数都是封装在body中,需要先从body中取出业务参数,然后再针对性解析,这时候Spring提供的解析器就帮不了我们了,需要我们扩展自定义适配参数解析器了。

Spring提供两种自定义参数解析器的方式,一种是实现适配器接口
HandlerMethodArgumentResolver
,另一种是继承已有的参数解析器(
HandlerMethodArgumentResolver
接口的现有实现类)例如
AbstractNamedValueMethodArgumentResolver
进行增强优化。如果是深度定制化的自定义参数解析器,建议实现自己实现接口进行开发,以实现接口适配器接口自定义开发解析器为例,介绍如何自定义一个参数解析器。

通过查看源码发现,参数解析适配器接口留给我扩展的方法有两个,分别是supportsParameter( )和resolveArgument( ),第一个方法是自定义参数解析器适用的场景,也就是如何命中参数解析器,第二个是具体解析参数的实现。

public interface HandlerMethodArgumentResolver {

   /**
    * 识别到哪些参数特征,才使用当前自定义解析器
    */
   boolean supportsParameter(MethodParameter parameter);

   /**
    * 具体参数解析方法
    */
   Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;

}




现在开始具体实现一个基于注解的自定义参数解析器,这个是代码实际使用过程中用到的参数解析器,获取color网关的body业务参数,然后解析后给Controller方法直接使用。

public class ActMethodArgumentResolver implements HandlerMethodArgumentResolver {
    private static final String DEFAULT_VALUE = "body";

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        /** 只有指定注解注释的参数才会走当前自定义参数解析器 */
        return parameter.hasParameterAnnotation(RequestJsonParam.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        /** 获取参数注解 */
        RequestJsonParam attribute = parameter.getParameterAnnotation(RequestJsonParam.class);
        
        /** 获取参数名 */
        String name = attribute.value();
        /** 获取指定名字参数的值 */
        String value = webRequest.getParameter(StringUtils.isEmpty(name) ? DEFAULT_VALUE : name);
        /** 获取注解设定参数类型 */
        Class<?> targetParamType = attribute.recordClass();
        /** 获取实际参数类型 */
        Class<?> webParamType = parameter.getParameterType()
        /** 以自定义参数类型为准 */
        Class<?> paramType = targetParamType != null ? targetParamType : parameter.getParameterType();
        if (ObjectUtils.equals(paramType, String.class) 
            || ObjectUtils.equals(paramType, Integer.class)
            || ObjectUtils.equals(paramType, Long.class) 
            || ObjectUtils.equals(paramType, Boolean.class)) {
                JSONObject object = JSON.parseObject(value);
                log.error("ActMethodArgumentResolver resolveArgument,paramName:{}, object:{}", paramName, JSON.toJSONString(object));
                if (object.get(paramName) instanceof Integer && ObjectUtils.equals(paramType, Long.class)) {
                    //入参:Integer  目标类型:Long
                    result = paramType.cast(((Integer) object.get(paramName)).longValue());
                }else if (object.get(paramName) instanceof Integer && ObjectUtils.equals(paramType, String.class)) {
                    //入参:Integer  目标类型:String
                    result = String.valueOf(object.get(paramName));
                }else if (object.get(paramName) instanceof Long && ObjectUtils.equals(paramType, Integer.class)) {
                    //入参:Long  目标类型:Integer(精度丢失)
                    result = paramType.cast(((Long) object.get(paramName)).intValue());
                }else if (object.get(paramName) instanceof Long && ObjectUtils.equals(paramType, String.class)) {
                    //入参:Long  目标类型:String
                    result = String.valueOf(object.get(paramName));
                }else if (object.get(paramName) instanceof String && ObjectUtils.equals(paramType, Long.class)) {
                    //入参:String  目标类型:Long
                    result = Long.valueOf((String) object.get(paramName));
                } else if (object.get(paramName) instanceof String && ObjectUtils.equals(paramType, Integer.class)) {
                    //入参:String  目标类型:Integer
                    result = Integer.valueOf((String) object.get(paramName));
                } else {
                    result = paramType.cast(object.get(paramName));
                }
        }else if (paramType.isArray()) {
            /** 入参是数组 */
            result = JsonHelper.fromJson(value, paramType);
            if (result != null) {
                Object[] targets = (Object[]) result;
                for (int i = 0; i < targets.length; i++) {
                   WebDataBinder binder = binderFactory.createBinder(webRequest, targets[i], name + "[" + i + "]");
                   validateIfApplicable(binder, parameter, annotations);
                }
             }
       } else if (Collection.class.isAssignableFrom(paramType)) {
            /** 这里要特别注意!!!,集合参数由于范型获取不到集合元素类型,所以指定类型就非常关键了 */
            Class recordClass = attribute.recordClass() == null ? LinkedHashMap.class : attribute.recordClass();
            result = JsonHelper.fromJsonArrayBy(value, recordClass, paramType);
            if (result != null) {
               Collection<Object> targets = (Collection<Object>) result;
               int index = 0;
               for (Object targetObj : targets) {
                   WebDataBinder binder = binderFactory.createBinder(webRequest, targetObj, name + "[" + (index++) + "]");
                   validateIfApplicable(binder, parameter, annotations);
               }
            }
        } else{
              result = JSON.parseObject(value, paramType);
        }
    
        if (result != null) {
            /** 参数绑定 */
            WebDataBinder binder = binderFactory.createBinder(webRequest, result, name);
            result = binder.convertIfNecessary(result, paramType, parameter);
            validateIfApplicable(binder, parameter, annotations);
            mavContainer.addAttribute(name, result);
        }
    }



自定义参数解析器注解的定义如下,这里定义了一个比较特殊的属性recordClass,后续会讲到是解决什么问题。

/**
 * 请求json参数处理注解
 * @author wangpengchao01
 * @date 2022-11-07 14:18
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestJsonParam {
    /**
     * 绑定的请求参数名
     */
    String value() default "body";

    /**
     * 参数是否必须
     */
    boolean required() default false;

    /**
     * 默认值
     */
    String defaultValue() default ValueConstants.DEFAULT_NONE;

    /**
     * 集合json反序列化后记录的类型
     */
    Class recordClass() default null;
}



通过配置类将自定义解析器注册到Spring容器中

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Bean
    public static ActMethodArgumentResolver actMethodArgumentResolverConfigurer() {
        return new ActMethodArgumentResolver();
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(actMethodArgumentResolverConfigurer());
    }
}



到此,一个完整的基于注解的自定义参数解析器就完成了。

4.总结

了解Spring的参数解析器原理有助于正确使用Spring的参数解析器,也让我们可以设计适用于自身系统的参数解析器,对于一些通用参数类型的解析减少重复代码的书写,但是这里有个前提是我们
项目中复杂类型的入参要统一

前端传递参数的格式也要统一
,不然设计自定义参数解析器就是个灾难,需要做各种复杂的兼容工作。参数解析器的设计尽量要放在项目开发开始阶段,历史复杂的系统如果接口开发没有统一规范也不建议自定义参数解析器设计。

该文章仅作为Spring参数解析器的介绍性解读,希望对大家有所帮助,欢迎有这类需求或者兴趣的同学沟通交流,批评指正,一起进步!

本文适用于电脑有GPU(显卡)的同学,没有的话直接安装cpu版是简单的。CUDA是系统调用GPU所必须的,所以教程从安装CUDA开始。

可以配合视频教程食用:
https://www.bilibili.com/video/BV12m4y1m7pq/?vd_source=06e4e8652ea90d79dadb7a59ff8acd36

CUDA安装

CUDA是加速深度学习计算的工具,诞生于NVIDIA公司,是一个显卡的附加驱动。必须使用
NVIDIA的显卡才能安装
,可以打开任务管理器查看自己的硬件设备。

下载CUDA

简便起见,可以直接通过我提供的下载链接进行下载(V11.6)

通过这个链接可以下载任意CUDA版本:
CUDA Toolkit Archive | NVIDIA Developer

我下载的是这一个:
https://developer.download.nvidia.com/compute/cuda/11.6.2/local_installers/cuda_11.6.2_511.65_windows.exe

下载任意版本cuDNN(需要注册英伟达俱乐部才能直接下载):
cuDNN Archive | NVIDIA Developer

我下载的是这一个:
https://developer.nvidia.com/compute/cudnn/secure/8.4.1/local_installers/11.6/cudnn-windows-x86_64-8.4.1.50_cuda11.6-archive.zip

推荐使用
迅雷
进行下载,更详细的下载教程参考:

cuda安装 (windows版)_何为xl的博客-CSDN博客_windows安装cuda](
https://blog.csdn.net/weixin_43848614/article/details/117221384
)

CUDA的版本选择是跟显卡型号有关还是驱动有关?

一般是
驱动版本
决定了能用的CUDA版本的
上限
,比如新版的显卡驱动可以支持老的CUDA runtime。但是老的显卡可能无法更新到最新的显卡驱动

安装CUDA

这边演示
CUDA11.6版本
的安装。注意:
30系列的显卡必须使用CUDA11.0以上的版本
,其他显卡既可以使用10也可以使用11版本。

双击打开安装包(这个路径就别动了)

CUDA是加速深度学习计算的工具,诞生于NVIDIA公司,是一个显卡的附加驱动。必须使用
NVIDIA的显卡才能安装
,可以打开任务管理器查看自己的硬件设备。这边演示
CUDA11.6版本
的安装。注意:
30系列的显卡必须使用CUDA11.0以上的版本
,其他显卡既可以使用10也可以使用11版本。

双击打开安装包(默认安装路径就别修改了)

点击同意并继续

选择自定义,点击下一步

这里可以更改安装路径,可以新建2个文件夹,将
CUDA Documentation

CUDA Development
安装在一个文件夹
CUDA

Samples
安装在另一个文件夹
sample
。我是安装在
D盘
的,这样可以节省C盘空间,不过需要记得之后修改环境变量修改为相应文件目录。

img

可以使用cmd,输入:

nvcc -V

查看CUDA是否安装成功

用cudnn打补丁

CUDA需要配合cudnn才能正常工作,将cudnn的四个文件(夹),复制到CUDA的安装路径即可,即CUDA文件夹。

将之前下载的压缩包解压到CUDA文件夹里面


添加环境变量

往系统环境变量中的 path 添加如下路径(
根据自己的路径进行修改

C:\Program Files\NVIDIA\CUDA\bin
C:\Program Files\NVIDIA\CUDA\include
C:\Program Files\NVIDIA\CUDA\lib
C:\Program Files\NVIDIA\CUDA\libnvvp

环境变量配置结果应当如下:

检验:

配置完成后,我们可以验证是否配置成功,主要使用CUDA内置的
deviceQuery.exe

bandwidthTest.exe

首先win+R启动cmd,cd到
安装目录
下的
…\extras\demo_suite
,然后分别执行
.\bandwidthTest.exe

.\deviceQuery.exe
,应该得到下图.

cd C:\Program Files\NVIDIA\CUDA\extras\demo_suite
.\bandwidthTest.exe
.\deviceQuery.exe


CUDA安装到这里就全部完成了.

安装anaconda

官网下载anaconda速度太慢,建议使用镜像网站下载,另外不建议下载最新的anaconda版本,默认的base环境是python 3.10,建议下载早一点的版本(22年),安装没有其他注意事项,按照默认设置安装即可。

清华anaconda镜像下载网站:
https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/?C=M&O=D

Pytorch安装

首先前往Pytorch官网查找适合自己CUDA版本的安装命令。安装命令分为conda命令和pip命令,conda命令不能手动添加镜像,需要更改配置文件,在已经安装好CUDA的基础上可以直接使用pip命令成功率较高(pip命令网络更好)。

Pytorch下载官网:
Previous PyTorch Versions | PyTorch

对于我们安装的CUDA11.6,在anaconda提供的命令行工具
Anaconda Powershell Prompt
中输入以下命令进行安装

pip install torch==1.13.1+cu116 torchvision==0.14.1+cu116 torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cu116

成功安装后进行检验:

python
import torch
torch.cuda.is_available()

如果结果为True那么证明GPU版本的pytorch已经安装完成

TensorFlow安装

TensorFlow更新速度较慢,安装命令是通用的,可以尝试在当前环境直接安装,如果报错直接尝试第二种conda安装命令。

pip install --ignore-installed --upgrade tensorflow-gpu

但是通过手动安装的方式可能会遇到许多问题,包括但不限于c++库有问题.

首先,我想说的是,千万不要用pip安装tensorflow-gpu,因为gpu版本需要很多依赖包,包括cuda、cudnn等,而且,就算你单独去下载这些,安装后还是会有很多配置问题导致有误,无法安装成功,所以直接用conda安装(这里留下了一行行泪水)。

tensorflow-gpu完整安装(附各种报错解决办法)_恩泽君的博客-CSDN博客

因为conda自带的安装源速度特别慢,所以如果你不设置镜像源下载几乎还是安装不了,中途会自动断开,这里我选择清华镜像源,首先打开anaconda prompt,依次输入以下四个指令添加镜像源。

conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --set show_channel_urls yes
conda config --show channels

成功率最高的安装方式
是:这一行代码安装基本不会报错。

conda create -n tensorflow-gpu tensorflow-gpu

安装完成后进行测试:

conda activate tensorflow-gpu
python
import tensorflow as tf
tf.test.is_gpu_available()

参考文章:

关于国内anaconda镜像站点看这一篇就够啦 - 知乎 (zhihu.com)

已解决error: subprocess-exited-with-error_袁袁袁袁满的博客-CSDN博客

Microsoft C++ 生成工具 - Visual Studio

(1条消息) Tensorflow-gpu安装超详细!!!_tensorflow gpu_东海扬尘_BingO的博客-CSDN博客

ABPvNext-微服务框架基础入门

本文使用的是ABPvNext商业版 最新稳定版本7.0.2为演示基础的,后续如果更新,会单独写一篇最新版本的,此文为零基础入门教程,后续相关代码会同步更新到gitee仓库中。


准备工作:

  • 1.登录ABPvNext官网 网址
    http://abp.io

  • 2.跳转到商业版的说明文档,目前商业版没有中文,只能使用谷歌浏览器的内置翻译功能了

  • 3.框架的相关环境要求,请自自行查看


  • 4.安装ABP CLI工具,安装方式是使用dotnet命令行工具,作为.NET开发者,这个本机一定是有的了

    //安装或者更新构建工具,如果想与我这篇文章一样,请指定安装版本号
    dotnet tool install -g Volo.Abp.Cli
    
    //登录ABP官网账号,既然使用商业版,肯定是已经有相关账号了
    abp login <username>
    
    //安装 ABP 套件(主要功能是后续创建解决方案以及代码生成工具等)
    abp suite install
    
    
    

创建项目并启动:

  • 1.通过ABP提供的商业套件 新建一个项目,本次使用图形化方式创建

    //执行完成后会拉起一个控制台,和网页,不要关闭控制台
    abp suite
    
  • 2.在打开的页面中,点击 Create a New solution(创建一个新的解决方案)

  • 3.创建使用微服务模板创建,所以选择Microservice template

  • 4.分别输入项目名称(Project Name) 输出目录(Output folder)以及UI模式(MVC),点击确定,开始 创建项目

  • 5项目生成完成后,打开相关目录,可以看到工具给生成的ABPvNext微服务项目

  • 6.接下来,我们将run-tye.ps1文件打开,需要修改一下其中的一些配置(更改前)

    <# Check development certificates #>
    <# 本地的证书,由于项目分层,之间调用都是使用https方式,所以本地调试必须有证书 #>
    if (! (  Test-Path ".\etc\dev-cert\localhost.pfx" -PathType Leaf ) ){
       Write-Information "Creating dev certificates..."
       cd ".\etc\dev-cert"
       .\create-certificate.ps1
       cd ../..  
    }
    
    <# 这个是官方提供的运行环境,这个建议大家单独安装,否则开发机器会很卡,所以直接删除掉 #>
    
    $requiredServices = @(
      'sql-server-db',
      'grafana',
      'prometheus',
      'kibana',
      'rabbitmq',
      'elasticsearch',
      'redis'
    )
      
    foreach ($requiredService in $requiredServices) {  
    
        $nameParam = -join("name=", $requiredService)
      $serviceRunningStatus = docker ps --filter $nameParam
      $isDockerImageUp = $serviceRunningStatus -split " " -contains $requiredService
      
      if( $isDockerImageUp )
      {
        Write-Host ($requiredService + " [up]")
      }
      else
      {
          cd "./etc/docker/"
        docker network create abpmicservie-network
        docker-compose -f docker-compose.infrastructure.yml -f docker-compose.infrastructure.override.yml up -d
        cd ../..
        break;
      }
    }
    <# 这个后面会手动执行,所以也删除掉 #>
    cd "./shared/ABPMicServie.DbMigrator"
    dotnet run
    cd ../..
     
    
    <# Run all services #>
    
    tye run --watch
    
  • 我们将文件重命名为 运行.ps1,内容如下

    <# Check development certificates #>

    if (! ( Test-Path ".\etc\dev-cert\localhost.pfx" -PathType Leaf ) ){

    Write-Information "Creating dev certificates..."

    cd ".\etc\dev-cert"

    .\create-certificate.ps1

    cd ../..

    }

    <# Run all services #>

    tye run --watch

  • 7.先不要运行项目,在项目的根目录打开PowerShell,运行dotnet build命令,整体还原项目包

  • 8.打开项目,更改 shared/ABPMicServie.DbMigrator 项目的数据库链接字符串

    原始项目需要修改四个数据库,每个数据库需要手动建立完成

  • 9.在数据库中分别建立四个数据,然后更改项目中链接字符串的IP,用户名等

  • 10.在ABPMicServie.DbMigrator项目右键→调试→启动新示例,运行迁移项目,如果链接字符串设置的没问题,执行成功后,数据库中相应表就应该已经生成完成,可以查看一下数据库,看看相应的表是否已经创建完成

  • 11.接下来就是更改各个项目的链接字符串 ,根据原有链接修改:


    • ABPMicServie.AuthServer
    • ABPMicServie.AdministrationService.HttpApi.Host
    • ABPMicServie.IdentityService.HttpApi.Host
    • ABPMicServie.ProductService.HttpApi.Host
    • ABPMicServie.SaasService.HttpApi.Host
  • 12.接下来要修改各个项目的中间件配置,包括 RabbitMQ,Redis,ElasticSearch等,由于修改基本涵盖了,apps,gateways,services三个目录下的所有项目,就不一一列举了

  • 13.安装tye,也是直接只用dotnet的控制台工具

    dotnet tool install -g Microsoft.Tye --version "0.11.0-alpha.22666666.1"
    
  • 14.安装完成后,直接在项目目录的运行.ps1文件单击右键→使用powershell运行,出现下面窗口

  • 15.在浏览器中打开 localhost:8000,出现tye管理界面,具体每个服务的端口,日志都可以查看

  • 16.访问web项目对应的端口,我这次对应的端口为
    https://localhost:44321/
    ,看到这个界面说明web服务已正常运行,接下来我们点击登录,进入登录界面

  • 17.微服务模式的登陆使用的统一认证方式,所以在登录界面实际是调用了ABPMicServie.AuthServer相关的服务,默认的管理用户名:admin 密码:1q2w3E* 这个密码是在迁移文件中设置的



  • 18.登录成功,看到如下页面,就说明ABPvNext的微服务框架已顺利执行成功,撒花

目录

一: 两种本地与远程仓库同步

"""
1)你作为项目仓库初始化人员:
线上要创建空仓库 => 本地初始化好仓库 => 建立remote链接(remote add) => 提交本地仓库到远程(push)

2)你作为项目后期开发人员:
远程项目仓库已经创建成功 => 复制远程仓库到本地(clone) => 进入仓库就可以进行接下来的开发
"""

1 git 远程仓库

1.创建一个空项目,如果选择.gitignore和开源许可,那么创建的创建就不是一个空仓库,会存在:
	1 .gitignore
	2 LICENSE  开源许可

image

2.创建完成的空仓库

image

提交本地版本库操作
1.初始化
git init
2.查看当前状态
git status
3.提交到暂存区
git add .
4.查询当前状态
git status
5.提交到本地的版本库
git commit -m '第一次提交'

image

提交到远程版本库操作
1.Git 全局设置:
git config --global user.name "www.zhang.com"
git config --global user.email "10366666612+wwwzhang-com@user.noreply.gitee.com"
# 注意: 如果邮箱设置的是别人的邮箱,那么提交到远程仓库就会显示别人的头像与名称
git config --global user.email "10366666612+wwwzhang-com@user.noreply.gitee.com"
2.增加一个远程仓库地址
git remote add origin https://gitee.com/wwwzhang-com/admin_item.git

# 提示标准写法:
	git remote add 名字 远程仓库地址

image

3.查询当前存在的远程仓库
git remote

image

5.本地版本库内容提交到远程仓库
git push origin master  # 把本地版本库内容,提交到远程仓库,需要输入giee的用户名密码

# 提示标准写法:
	git push 远程仓库名字 分支名字

image

image

6.查询远程仓库

image

注意:本地版本库提交到远程仓库用户名密码存放位置

image

远程仓库操作

1.增加
-git remote add 名字 远程仓库地址
2.查看
-git remote
3.删除
-git remote remove origin
4.提交到远程仓库
-git push origin master

本地仓库与远程仓库提交使用(删除与新增)

1.当pycharm删除了一个文件s1.py
# 当pycharm删除了一个文件s1.py
1.删除或者新增都会进缓存区
2.将缓存区提交到本地版本库
git commit -m '删除s1.py'

image

2.本地版本库内容提交到远程仓库
push origin master

image

3.查询远程仓库
1.本地版本库提交到远程仓库记录
2.分支master提交到远程仓库记录,测试,开发本地版本库。提交到远程仓库。

image

remote源操作

"""
1)查看仓库已配置的远程源
>: git remote
>: git remote -v

2)查看remote命令帮助文档
>: git remote -h

3)删除远程源
>: git remote remove 源名
eg: git remote remove origin

4)添加远程源
>: git remote add 源名 源地址
>: git remote add orgin git@gitee.com:doctor_owen/luffyapi.git

5)提交代码到远程源
>: git push 源码 分支名

6)克隆远程源
>: git clone 远程源地址
"""

"""
1)通过克隆方式连接远程源,默认远程源名就叫origin;所以主动添加远程源来连接远程仓库,源码也用origin
2)本地一个仓库只需要和远程一个源进行同步,所以只需要连接一个远程源,如果还想把本地代码同步给第三个平台,那么可以通过主动添加远程源来连接第三个平台的远程仓库,此时的源码就不能再叫origin了,比如online都可以
3)pull和push都可以提供选择不同的源码,和不同的远程仓库交互
"""

2 采用ssh协议连接远程

连接远程服务器的两种方式
1.https方式: 使用用户名+密码认证的方式
2.ssh方式: 公钥私钥方式
    1.本地生成公钥私钥
    2.把公钥配置到gitee上
    3.以后再提交到远程仓库就不需要输入用户名和密码了
使用ssj协议连接远程使用流程
1.cmd命令 窗口执行生成公钥私钥
# 本地生成一个公钥私钥 rsa加密方式  邮箱
ssh-keygen -t rsa -C "10366666612+wwwzhang-com@qq.com"

# 生成存储位置: C:\Users\86130/.ssh/id_rsa
生成到用户家路径的 .ssh文件夹下
-id_rsa      # 私钥
-id_rsa.pub  # 公钥

# 回车即可

image

2.把本地公钥配置到gitee上

image

重写配置远程仓库为ssh(以后再提交代码就不需要输入用户名密码了)

# 注意: 本地sttps,git的凭据就可以删除了

# 1.删除之前配置的https协议的远程仓库
git remote remove origin

# 2.查看当前远程仓库
git remote

# 3.配置ssh远程仓库(以后再提交代码就不需要输入用户名密码了)
git remote add origin git@gitee.com:wwwzhang-com/lufy_api.git

# 4.查询当前远程仓库   
git remote

image

本地版本库提交到远程ssh仓库

# 1.修改了gitignore内容(提交至缓存区)
git add .
# 2.将缓存区提交至版本库
git commit -m '修改了gitignore'
# 3.将本地版本库提交至远程ssh仓库
git push origin master  # yes,继续连接

image

3 项目创始者和项目开发者

1.如果我是项目开发者,需要把代码clone到本地

# 开发者
1)生成自己电脑的公钥,配置到自己的码云个人公钥中
2)把自己的码云用户名提供给别人,别人添加你成为项目开发者
3)自己确认添加,成为开发者,克隆开发项目,进行开发
# 需求: 本地lufei项目被意外删除了,需要从远程仓库克隆/下载到本地

1.https或ssh或克隆都可以,三种下载方式
# git clone https链接
git clone https://gitee.com/wwwzhang-com/lufy_api.git

image

下载本地成功

image

# 所有版本记录都存放在.git文件夹里面
git status

image

重新将代码提交到远程仓库
# 修改代码
1.提交至缓存区
git add .
2.提交至版本库
git commit -m '开发者最后'
3.提交至远程仓库  
git push origin master  

# 注意
1.出现输入用户名和密码的情况,输入即可,因为是使用https下载的
# 解决方法:
1.重新配置ssh公钥私钥即可

image

1 如果我是项目开发者,需要把代码clone到本地
	-git clone https://gitee.com/liuqingzheng/luffy_api.git
     -修改代码
    -提交到远程
    	git push origin master

2 如果我是项目创始者,需要远程新建仓库,本地代码push上去

# 创始者顺序
1)生成自己电脑的公钥(公钥生成一次就可以了),把它提交给项目管理者
2)项目管理者添加你公钥,加入开发者,提供给你项目克隆地址
3)克隆开发项目,进行开发

# 1.项目的创始者:
可以在项目管理-->仓库成员管理-->开发者邀请其他成员加入--成为开发者

# 2.开发者可以进行以下步骤:
    1.我是项目的开发者,把代码clone到本地
    2.修改代码
    3.提交到远程

image

4 协同开发

1 管理员,开发者(10开发者)
	-能提交代码(能增删查改代码)  

协同开发(跨版本问题)

# 协同开发(跨版本问题)
1.当开发者在开发a版本时,另两个开发者,已经提交了两个版本到远程仓库,
2.问题: 这个时候在开发a版本分支的开发者如果提交到远程仓库会造成版本冲突,严重会让2版本和3版本被自己跨版本提交的版本覆盖掉。
3.解决: 必须先拉取代码下来(让你本地保持最新)git pull origin master,在进行提交即可  

image

协同开发(拉取代码与自己相同问题)

1.可能会发送的冲突,当我们拉取下来的代码与自己修改的是同一行代码,会发送冲突
2.所以解决冲突,再进行提交即可

image

协同开发(拉取代码与自己相同问题)解决上述问题完整

1.冲突解决(多人在同一个分支开发,导致冲突)
"""
<<<<<<< HEAD  # 冲突的开始
# 自己的代码
=======  # 分割线
# 别人的代码
>>>>>>> b63c408abd05b87fc492d40523240561999dba50  # 冲突的结束(版本)
"""
# 冲突解决:
	解决方案一:
    1.改同样的功能出现的冲突,看一下谁的代码更好,保留好代码,删除不好代码
    解决方案二:
    2.改的不是同样的功能出现了冲突,留着两个人的
    
# 避免冲突
	1.没事就拉一下代码

5 线上分支合并

1.本地建立分支,同步到远端

# 本地建立分支,同步到远端
-git branch dev
-git push origin dev
本地建立分支,同步到远端顺序
1.创建dev分支
git branch dev

2.切换到dev分支
git checkout dev

3.提交至版本库
git commit -m 'dev分支更改'

4.将本地分支dev提交到远端的master下
git push origin dev

image

引入前提远端同步本地:
## 本地:
# 1.切换到master分支
git checkout master
# 2.强删除dev分支(因为没有合并普通删除不了,使用-f 强制)
git branch -d dev -f

image

## 远端
1.删除上一次使用的dev分支
2.重新创建一个新的dev分支

image

1.远端建立分支,同步到本地

# 远端建立分支,同步到本地
-git pull origin dev
-git branch   # 看不到
-git checkout dev  #切到dev分支,本地就能看到了
远端建立分支,同步到本地顺序
1.拉取远端master分支(没事拉一下防止冲突)
git pull origin master

2.查看当前分支
git branch

3.将远端分支dev拉取到本地
git pull origin dev  # 注意因为当前dev分支什么都没有,所以可能git branch查询不到,直接切换即可,在git branch就跨域看到了。

image

本地分支提交代码到远端dev分支
1.切换到dev分支
git checkout dev  

2.增加数据

3.提交到缓存区
git add .

4.提交至版本库
git commit -m '提交'

5.将本地dev分支提交至远端dev分支
git push origin dev

5.查询当前分支
git branch

image