Spring自定义参数解析器设计

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
@PathVariablePathVariableMethodArgumentResolver
@RequestParamRequestParamMethodArgumentResolver无(get请求)和multipart/form-data
@RequestBodyRequestResponseBodyMethodProcessorapplication/json
@RequestPartRequestPartMethodArgumentResolvermultipart/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参数解析器的介绍性解读,希望对大家有所帮助,欢迎有这类需求或者兴趣的同学沟通交流,批评指正,一起进步! 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/65581.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序--原生

1&#xff1a;数据绑定 1&#xff1a;数据绑定的基本原则 2&#xff1a;在data中定义页面的数据 3&#xff1a;Mustache语法 4&#xff1a;Mustache的应用场景 1&#xff1a;常见的几种场景 2&#xff1a;动态绑定内容 3&#xff1a;动态绑定属性 4&#xff1a;三元运算 4&am…

【阻止IE强制跳转到Edge浏览器】

由于微软开始限制用户使用Internet Explorer浏览网站&#xff0c;IE浏览器打开一些网页时会自动跳转到新版Edge浏览器&#xff0c;那应该怎么禁止跳转呢&#xff1f; 1、点击电脑左下角的“搜索框”或者按一下windows键。 2、输入“internet”&#xff0c;点击【Internet选项…

【Yolov5+Deepsort】训练自己的数据集(1)| 目标检测追踪 | 轨迹绘制

&#x1f4e2;前言&#xff1a;本篇是关于如何使用YoloV5Deepsort训练自己的数据集&#xff0c;从而实现目标检测与目标追踪&#xff0c;并绘制出物体的运动轨迹。本章讲解的为第一个内容&#xff1a;简单介绍YoloV5Deepsort中所用到的目标检测&#xff0c;追踪及sort&Depp…

什么是进程、线程、协程

什么是进程&#xff1f; 我们都知道计算机的核心是CPU&#xff0c;它承担了所有的计算任务&#xff1b;而操作系统是计算机的管理者&#xff0c;它负责任务的调度、资源的分配和管理&#xff0c;统领整个计算机硬件&#xff1b;应用程序则是具有某种功能的程序&#xff0c;程序…

CSS 的选择器有哪些种类?分别如何使用?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 元素选择器&#xff08;Element Selector&#xff09;⭐ 类选择器&#xff08;Class Selector&#xff09;⭐ ID 选择器&#xff08;ID Selector&#xff09;⭐ 后代选择器&#xff08;Descendant Selector&#xff09;⭐ 子元素选择器&a…

嵌入式开发学习(STC51-7-矩阵按键)

内容 按下S1-S16键&#xff0c;对应数码管最左边显示0-F 矩阵按键简介 独立按键与单片机连接时&#xff0c;每一个按键都需要单片机的一个I/O 口&#xff0c;若某单片机系统需较多按键&#xff0c;如果用独立按键便会占用过多的I/O口资源&#xff1b;而单片机 系统中I/O口资…

C++利用mutex和thread实现一个死锁

程序 #include<iostream> #include<mutex> #include<thread> using namespace std; mutex mtx1; mutex mtx2; void A(){mtx1.lock();cout<<"a:mtx1"<<endl;this_thread::sleep_for(chrono::milliseconds(1000));mtx2.lock();cout<…

【Sa-Token】9、Sa-Token实现在线用户管理功能

尽管框架将大部分操作提供了简易的封装&#xff0c;但在一些特殊场景下&#xff0c;我们仍需要绕过框架&#xff0c;直达数据底层进行一些操作。 1、官方文档 会话查询 https://sa-token.cc/doc.html#/up/search-sessionSa-Token提供以下API助你直接操作会话列表&#xff1a…

【C++基础】友元总结一些坑

友元类 友元类&#xff08;Friend Class&#xff09;是一种在C中用于实现类之间访问权限的特殊机制。通过友元类&#xff0c;一个类可以允许另一个类访问其私有成员&#xff0c;甚至可以使另一个类成为其友元&#xff0c;使其能够访问所有成员&#xff0c;包括私有成员。这种机…

python爬虫2:requests库-原理

python爬虫2&#xff1a;requests库-原理 前言 ​ python实现网络爬虫非常简单&#xff0c;只需要掌握一定的基础知识和一定的库使用技巧即可。本系列目标旨在梳理相关知识点&#xff0c;方便以后复习。 目录结构 文章目录 python爬虫2&#xff1a;requests库-原理1. 概述2. re…

学习笔记-JVM-对象结构及生命周期

申明&#xff1a;文章内容是本人学习极客时间课程所写&#xff0c;文字和图片基本来源于课程资料&#xff0c;在某些地方会插入一点自己的理解&#xff0c;未用于商业用途&#xff0c;侵删。 原资料地址&#xff1a;课程资料 对象的创建流程 常量池检查:检查new指令是否能在常…

【设计模式——学习笔记】23种设计模式——中介者模式Observer(原理讲解+应用场景介绍+案例介绍+Java代码实现)

文章目录 案例引入案例一普通实现中介者模式 案例二 介绍基础介绍登场角色尚硅谷 《图解设计模式》 案例实现案例一&#xff1a;智能家庭类图实现 案例二&#xff1a;登录页面逻辑实现说明类图实现 总结文章说明 案例引入 案例一 普通实现 在租房过程中&#xff0c;客户可能…

2.5D游戏是如何做出来的呢,2.5D游戏快速制作教程

前言 【Unity实战篇 】 | 如何制作一款2.5D游戏&#xff0c;2.5D游戏制作案例一、2.5D 游戏概念二、绘制地图三、添加玩家动画和移动等操作四、视角配置4.1 调整摄像机与场景对象的角度4.2 增加镜头旋转功能 五、游戏效果展示 总结 前言 玩过游戏的朋友都知道&#xff0c;市面…

HarmonyOS NEXT,生命之树初长成

在不同的神话体系中&#xff0c;都有着关于生命之树的记载。 比如在北欧神话中&#xff0c;一株巨大的树木联结着九大世界&#xff0c;其被称为“尤克特拉希尔”Yggdrasill。在中国的《山海经》中&#xff0c;也有着“建木”的传说&#xff0c;它“有九欘&#xff0c;下有九枸&…

idea添加翻译插件并配置有道翻译

1、安装Translation插件 2、 创建有道云应用 有道智云控制台 3、设置idea 4、效果&#xff08;选中文本右键翻译&#xff0c;默认快捷键CtrlShiftY&#xff09;

安达发制造工业迈向智能化:APS高级计划排程助力提升生产效率

随着市场竞争的加剧&#xff0c;制造企业纷纷寻求提高生产效率和降低成本的方法。近年来&#xff0c;越来越多的制造企业开始采用APS(高级计划与排程)系统&#xff0c;以优化生产计划和排程&#xff0c;提高生产效率&#xff0c;并在竞争中取得优势。 现代制造业通常面临复杂的…

Idea中maven无法下载源码

今天在解决问题的时候想要下载源码&#xff0c;突然发现idea无法下载&#xff0c;这是真的蛋疼&#xff0c;没办法查看原因&#xff0c;最后发现问题的原因居然是因为Maven&#xff0c;由于我使用的idea的内置的Bundle3的Maven&#xff0c;之前没有研究过本地安装和内置的区别&…

ESP 32 蓝牙虚拟键盘链接笔记本电脑的键值问题

由于打算利用esp32 通过蓝牙链接电脑后实现一些特俗的键盘功能&#xff0c;所以就折腾了一下&#xff0c;折腾最耗费时间的却是键值问题&#xff0c;让一个20多年的老司机重新补充了知识 过程曲折就不说了&#xff0c;直接说结果。 我们通过网络搜索获取的键值和蓝牙模拟键盘传…

Leetcode-每日一题【剑指 Offer 11. 旋转数组的最小数字】

题目 把一个数组最开始的若干个元素搬到数组的末尾&#xff0c;我们称之为数组的旋转。 给你一个可能存在 重复 元素值的数组 numbers &#xff0c;它原来是一个升序排列的数组&#xff0c;并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如&#xff0c;数组 [3,4…

【论文阅读】对抗溯源图主机入侵检测系统的模仿攻击(NDSS-2023)

作者&#xff1a;伊利诺伊大学芝加哥分校-Akul Goyal、Gang Wang、Adam Bates&#xff1b;维克森林大学-Xueyuan Han、 引用&#xff1a;Goyal A, Han X, Wang G, et al. Sometimes, You Aren’t What You Do: Mimicry Attacks against Provenance Graph Host Intrusion Detect…