spring.expression 随笔0 概述

news2025/1/24 2:55:16

0. 我只是个普通码农,不值得挽留

Spring SpEL表达式的使用
常见的应用场景:分布式锁的切面借助SpEL来构建key
比较另类的的应用场景:动态校验


个人感觉可以用作控制程序的走向,除此之外,spring的一些模块的自动配置类,也会在@Conditional注解中使用SpEL来实现有条件的加载特定的bean.

1. UML

1.1 ExpressionParser

解释器设计模式的体现了
在这里插入图片描述
单纯的(非模板表达式)spel表达式将通过 SpelExpressionParser 创建 InternalSpelExpressionParser, 来实现 parseExpression() 的底层逻辑.

	// org.springframework.expression.spel.standard.InternalSpelExpressionParser#doParseExpression
	@Override
	protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context)
			throws ParseException {

		try {
			this.expressionString = expressionString;
			// 1.对相关的符号进行分词
			Tokenizer tokenizer = new Tokenizer(expressionString);
			this.tokenStream = tokenizer.process();
			this.tokenStreamLength = this.tokenStream.size();
			this.tokenStreamPointer = 0;
			this.constructedNodes.clear();
			// 2.构建 AST
			SpelNodeImpl ast = eatExpression();
			Assert.state(ast != null, "No node");
			Token t = peekToken();
			if (t != null) {
				throw new SpelParseException(t.startPos, SpelMessage.MORE_INPUT, toString(nextToken()));
			}
			Assert.isTrue(this.constructedNodes.isEmpty(), "At least one node expected");
			// 3. 返回创建好的表达式实例
			return new SpelExpression(expressionString, ast, this.configuration);
		}
		catch (InternalParseException ex) {
			throw ex.getCause();
		}
	}

1.2 ParserContext

在这里插入图片描述

  • 可以得知: 表达式的上下文 也是 ParserContext 所给定的
  • TemplateAwareExpressionParser(支持模板的ExpressionParser实现类),根据 ParserContext.isTemplate()来决定处理流程
  • 有必要给出模板表达式的定义: 可以理解为多个、多种表达式的组合
	// org.springframework.expression.common.TemplateAwareExpressionParser#parseExpression(java.lang.String, org.springframework.expression.ParserContext)
	@Override
	public Expression parseExpression(String expressionString, @Nullable ParserContext context) throws ParseException {
		if (context != null && context.isTemplate()) {
			return parseTemplate(expressionString, context);
		}
		else {
			return doParseExpression(expressionString, context);
		}
	}

1.3 Expression

在这里插入图片描述

转换并获取表达式对应的数值

1.4 EvaluateContext

  • 支持间接的关联 beanFactory ,来注入spring bean
  • 该功能很好的体现了 spring-expression 的独立性
  • 支持往该上下文中加入静态方法、java bean
  • 与 Expression.getValue() 有较大的关系
    在这里插入图片描述

在这里插入图片描述

2. test-classes

因为要debug beanFactory关联的 parser,便懒得构造applicationContext,直接@SpringbootTest 启动容器了

@DisplayName("Spring Expression Language")
@SpringBootTest
public class SpELTest {

    @Value("#{testBean.value}")
    String value;

    @Autowired
    ApplicationContext appCtx;

    SpelExpressionParser parser;

    StandardEvaluationContext stdEvaCtx;

    @PostConstruct
    private void postConstruct() throws NoSuchMethodException {
        parser = new SpelExpressionParser();
        // rootObject
        stdEvaCtx = new StandardEvaluationContext(new TestBean("rootValue", null));
        // variable
        stdEvaCtx.setVariable("testBean", this.appCtx.getBean("testBean"));

        Method parseInt = Integer.class.getDeclaredMethod("valueOf", String.class);
        stdEvaCtx.registerFunction("doValueOf", parseInt);

        stdEvaCtx.setBeanResolver(new BeanFactoryResolver(this.appCtx));
    }

    @DisplayName("注解方式")
    @Test
    void t0() {
        // 这个上下文其实就是表达式、变量的容器(缓存)
        System.err.println(this.value);
    }

    @DisplayName("编码方式")
    @Test
    void t1() {
        // 不需要 {}
        // spring security 中也使用编码的方式解析权限注解 @PrePreAuthorize
        Expression expression = parser.parseExpression("#testBean.value");
        System.err.println(expression.getValue(this.stdEvaCtx));
    }

    @DisplayName("运算")
    @Nested
    class Count {

        @DisplayName("字面量")
        @Test
        void t0() {
            // 上下文中找不到这个变量,报错:
            // spel.SpelEvaluationException: EL1007E: Property or field 'test' cannot be found on null
            // System.err.println("找不到变量="+parser.parseExpression("test").getValue(String.class));

            // 字符串
            System.err.println("字符串1=" + parser.parseExpression("'test'").getValue(String.class));
            System.err.println("字符串2=" + parser.parseExpression("\"test\"").getValue(String.class));

            // 数字
            System.err.println("int=" + parser.parseExpression("1").getValue(Integer.class));
            System.err.println("long=" + parser.parseExpression("1L").getValue(long.class));
            System.err.println("float=" + parser.parseExpression("1.1").getValue(Float.class));
            System.err.println("double=" + parser.parseExpression("1.1E+1").getValue(double.class));
            System.err.println("hex=" + parser.parseExpression("0xf").getValue(int.class));

            // 布尔
            System.err.println("bool=" + parser.parseExpression("false").getValue(boolean.class));

            // null
            System.err.println(parser.parseExpression("null").getValue());
        }

        @DisplayName("算数")
        @Test
        void t1() {
            System.err.println("3+2=" + parser.parseExpression("3+2").getValue(Integer.class));
            System.err.println("3-2=" + parser.parseExpression("3-2").getValue(Integer.class));
            System.err.println("3*2=" + parser.parseExpression("3*2").getValue(Integer.class));
            System.err.println("3/2=" + parser.parseExpression("3/2").getValue(Integer.class));
            System.err.println("3%2=" + parser.parseExpression("3%2").getValue(Integer.class));
            System.err.println("3^2=" + parser.parseExpression("3^2").getValue(Integer.class));
        }

        @DisplayName("关系运算")
        @Test
        void t2() {
            System.err.println("3==2=" + parser.parseExpression("3==2").getValue(Boolean.class));
            System.err.println("3 == 2=" + parser.parseExpression("3 == 2").getValue(Boolean.class));
            System.err.println("3 ge 2 =" + parser.parseExpression("3 ge 2").getValue(boolean.class));
            System.err.println("3 LT 2 = " + parser.parseExpression("3 LT 2").getValue(boolean.class));

            // SpelParseException: EL1041E: After parsing a valid expression, there is still more data in the expression: 'NE2'
            // System.err.println("3NE2 = "+parser.parseExpression("3NE2").getValue(boolean.class));

            // 并不能返回 int:0、1,会抛出异常
            System.err.println("2 between {1, 2}=" + parser.parseExpression("2 between {1, 2}").getValue(Boolean.class));
            // SpelParseException: EL1041E: After parsing a valid expression, there is still more data in the expression: 'le(<=)'
            // System.err.println("1<2<=2="+parser.parseExpression("1<2<=3").getValue(Boolean.class));
        }

        @DisplayName("逻辑运算")
        @Test
        void t3() {
            System.err.println("2>1 and false = " + parser.parseExpression("2>1 and false").getValue(boolean.class));
            System.err.println("2>1 && false = " + parser.parseExpression("2>1 && false").getValue(Boolean.class));
            // SpelParseException: EL1041E: After parsing a valid expression, there is still more data in the expression: 'factory_bean_ref(&)'
            // System.err.println("2>1 & false = "+parser.parseExpression("2>1 & false").getValue(Boolean.class));
            System.err.println("2>1 or NOT false and (! NOT false || 1==1) = " + parser.parseExpression("2>1 or NOT false and (! NOT false || 1==1)").getValue(Boolean.class));
        }

        @DisplayName("三目运算")
        @Test
        void t4() {
            System.err.println("3 > 2 ? true : false = " + parser.parseExpression("3 > 2 ? true : false").getValue(boolean.class));
        }

        @DisplayName("elivis")
        @Test
        void t5() {
            System.err.println("null ?: 'false' = " + parser.parseExpression("null ?: 'false'").getValue(Boolean.class));
            System.err.println("null ?: 'false' = " + parser.parseExpression("null ?: 'false'").getValue(String.class));
        }

        @DisplayName("正则")
        @Test
        void t6() {
            System.err.println("" + parser.parseExpression("'123' matches '\\d{3}'").getValue(boolean.class));
            System.err.println("" + parser.parseExpression("123 matches '\\d{3}'").getValue(Boolean.class));
        }

        @DisplayName("instanceof")
        @Test
        void t7() {
            System.err.println("'123' instanceof T(String) = " + parser.parseExpression("'123' instanceof T(String)").getValue(Boolean.class));
            System.err.println("123 instanceof T(String) = " + parser.parseExpression("123 instanceof T(java.lang.String)").getValue(Boolean.class));
        }
    }

    @DisplayName("类型")
    @Nested
    class Type {

        @DisplayName("class")
        @Test
        void t0() {
            // java.lang 以外的类均需要全限定名
            System.err.println(parser.parseExpression("T(String)").getValue(Class.class));
            System.err.println(parser.parseExpression("T(java.util.Map)").getValue(Class.class));
            // 访问 静态的属性、方法
            System.err.println(parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.class));
            System.err.println(parser.parseExpression("T(Integer).parseInt(3)").getValue(Integer.class));
        }

        @DisplayName("instance")
        @Test
        void t1() {
            // java.lang 包 同理
            System.err.println(parser.parseExpression("new String('苹果一样的甜')").getValue(String.class));
            System.err.println(parser.parseExpression("new java.util.Date()").getValue(Date.class));
        }

        @DisplayName("reference")
        @Test
        void t2() {
            System.err.println("#testBean.value=" + parser.parseExpression("#testBean.value").getValue(stdEvaCtx, String.class));
            System.err.println("#this.value=" + parser.parseExpression("#this.value").getValue(stdEvaCtx, String.class));
            System.err.println("#root.value=" + parser.parseExpression("#root.value").getValue(stdEvaCtx, String.class));
            // rootObject缺省时,访问其属性,不能加#前缀
            System.err.println("(root属性可以省略#root)value=" + parser.parseExpression("value").getValue(stdEvaCtx, String.class));
        }

        @DisplayName("assign")
        @Test
        void t3() {
            System.err.println("#testBean.value=newValue --> " + parser.parseExpression("#testBean.value='newValue'").getValue(stdEvaCtx, String.class));
            System.err.println("#this.value=newThisValue --> " + parser.parseExpression("#this.value='newThisValue'").getValue(stdEvaCtx, String.class));
            System.err.println("#root.value=newRootValue --> " + parser.parseExpression("#root.value='newRootValue'").getValue(stdEvaCtx, String.class));
            System.err.println("value=newValue --> " + parser.parseExpression("value='newValue'").getValue(stdEvaCtx, String.class));
        }

        @DisplayName("func")
        @Test
        void t4() {
            System.err.println(parser.parseExpression("#doValueOf('20').byteValue()").getValue(stdEvaCtx, Byte.class));
        }

        @DisplayName("对象属性获取 及 安全导航")
        @Test
        void t5() {
            System.err.println(parser.parseExpression("value").getValue(stdEvaCtx, String.class));
            // Value 可以,Value 不得行(首字母不敏感)
            System.err.println(parser.parseExpression("Value").getValue(stdEvaCtx, String.class));

            // 支持groovy的elivis表达式
            // 安全导航运算符前面的#root可以省略,但后面的#root不可省略
            System.err.println(parser.parseExpression("#root?.#root").getValue(stdEvaCtx, TestBean.class));
            System.err.println(parser.parseExpression("value?.#root.value").getValue(stdEvaCtx, String.class));
            // SpelParseException: Expression [username?.'核弹拉链'] @8: EL1049E: Unexpected data after '.': ''核弹拉链''
            // SpEL引入了Groovy语言中的安全导航运算符"(对象|属性)?.属性"
            // 常量显然不得行
            // System.err.println(parser.parseExpression("username?.'核弹拉链'").getValue(stdEvaCtx, String.class));
        }

        @DisplayName("对象方法调用")
        @Test
        void t6() {
            System.err.println(parser.parseExpression("value.substring(1, 6).toUpperCase()").getValue(stdEvaCtx, String.class));
            System.err.println(parser.parseExpression("toString()").getValue(stdEvaCtx, String.class));
        }

        @DisplayName("bean引用(BeanFactoryResolver)")
        @Test
        void t7() {
            // @BeanName
            // EvaluationContext.setBeanResolver() 需要借助 beanFactory
            System.err.println(Jsons.NO_OP.stringify(parser.parseExpression("@systemProperties").getValue(stdEvaCtx, Properties.class)));
            System.err.println(Jsons.NO_OP.stringify(parser.parseExpression("@testBean").getValue(stdEvaCtx, TestBean.class)));
        }
    }

    @DisplayName("集合")
    @Nested
    class Collect {

        @DisplayName("内联数组")
        @Test
        void t0() {
            System.err.println(Arrays.toString(parser.parseExpression("new int[2]{1, 2}").getValue(int[].class)));
            System.err.println(Arrays.toString(parser.parseExpression("new String[2][2]").getValue(String[][].class)));
            // 不支持多维数组创建同时,做初始化
            System.err.println(Arrays.toString(parser.parseExpression("new String[2][2]{'1','2'},{'3','4'}").getValue(String[][].class)));
        }

        @DisplayName("内联集合")
        @Test
        void t1() {
            System.err.println(parser.parseExpression("{}").getValue(List.class));
            // java.util.Collections$UnmodifiableRandomAccessList
            System.err.println(parser.parseExpression("{1, 2,3}").getValue(List.class).getClass().getName());
            // 此时的 List<List> .class = java.util.ArrayList
            // 存在非字面量表达式时,集合类型将转为原始类型(可修改的集合)
            System.err.println(parser.parseExpression("{{1+2,2+4},{3,4+4}}").getValue(List.class).getClass().getName());
        }

        @DisplayName("数组、集合、字典元素访问")
        @Test
        void t2() {
            System.err.println(parser.parseExpression("[0]").getValue(new int[]{1, 2, 3}, Integer.class));
            System.err.println(parser.parseExpression("{1, 2, 3}[0]").getValue(int.class));
            System.err.println(parser.parseExpression("[0]").getValue(Lists.newArrayList(1, 2, 3), int.class));

            Map<String, Integer> map = Maps.newHashMap();
            map.put("weng", 4);
            map.put("chong", 5);
            map.put("yu", 2);
            System.err.println(parser.parseExpression("['chong']").getValue(map, int.class));
        }

        // 很像 streamApi.peek().collect(toList())
        @DisplayName("数组、集合、字典 转换")
        @Test
        void t3() {
            // array|list|map.![表达式]
            System.err.println(Arrays.toString(parser.parseExpression("#root.![#this+1]").getValue(new int[]{1, 2, 3}, int[].class)));
            System.err.println(parser.parseExpression("#root.![#this+1]").getValue(Lists.newArrayList(1, 2, 3), List.class));

            Map<String, Integer> map = Maps.newHashMap();
            map.put("weng", 4);
            map.put("chong", 5);
            map.put("yu", 2);
            System.err.println(Jsons.NO_OP.stringify(parser.parseExpression("#root.![#this.key+1]").getValue(map, List.class)));
            System.err.println(Jsons.NO_OP.stringify(parser.parseExpression("#root.![#this.value+1]").getValue(map, List.class)));
            // 报错: cannot convert from java.util.ArrayList<?> to java.util.Map<?, ?>
            // 集合、数组之间可以随意转换
            // System.err.println(Jsons.NO_OP.stringify(parser.parseExpression("#root.![#this.value+1]").getValue(map, Map.class)));
        }

        // 相当于 streamApi.filter.collect(toList)
        @DisplayName("数组、集合、字典 选择")
        @Test
        void t4() {
            // array|list|map.?[表达式]
            System.err.println(Arrays.toString(parser.parseExpression("#root.?[#this>=2]").getValue(new int[]{1, 2, 3}, int[].class)));
            System.err.println(Arrays.toString(parser.parseExpression("#root.?[#this>=2]").getValue(Lists.newArrayList(1, 2, 3), int[].class)));

            Map<String, Integer> map = Maps.newHashMap();
            map.put("weng", 4);
            map.put("chong", 5);
            map.put("yu", 2);
            // {"weng":4,"yu":2}
            System.err.println(Jsons.NO_OP.stringify(parser.parseExpression("#root.?[#this.key!='chong']").getValue(map, Map.class)));
            // 这里转的集合,有些怪异
            // [{"weng":4,"yu":2}]
            System.err.println(Jsons.NO_OP.stringify(parser.parseExpression("#root.?[#this.key!='chong']").getValue(map, List.class)));
            System.err.println(Jsons.NO_OP.stringify(parser.parseExpression("#root.?[#this.value<=2]").getValue(map, Map.class)));
        }
    }
}

附上相关的类

@Component("testBean")
@AllArgsConstructor
@NoArgsConstructor
@Data
public class TestBean {
    @Value("${angel.spel.key}")
    public String value;
    private String username;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chatgpt赋能python:Python如何判断输入数据类型

Python如何判断输入数据类型 Python是一种动态类型语言&#xff0c;它可以在运行时自动识别数据的类型。但是&#xff0c;有时候我们需要在代码中判断输入数据的类型&#xff0c;以便进行相应的操作。 判断数据类型的内置函数 Python有一些内置函数可以用于判断数据类型&…

仿滴滴打车百度地图定位查找附近出租车或门店信息

前端vue仿滴滴打车百度地图定位查找附近出租车或门店信息, 下载完整代码请访问uni-app插件市场地址:https://ext.dcloud.net.cn/plugin?id12982 效果图如下: # #### 使用方法 使用方法 #安装vue-baidu-map插件 npm install vue-baidu-map --save <!-- center: 地图中…

蓝牙耳机可以戴着游泳吗?推荐四款可以游泳的游泳耳机

今年的夏天格外炎热&#xff0c;热衷于户外运动的人们也开始转换健身方式&#xff0c;游泳作为一项锻炼全身又祛暑清凉的运动&#xff0c;自然成为了最佳选择&#xff0c;相信大多数人跟我一样在运动时都离不开耳机&#xff0c;而游泳需要和水接触&#xff0c;这也导致了我所有…

SpringBoot+Mybatis+Thymeleaf实现的物资管理系统

本系统具体使用的技术是&#xff1a;后端使用SpringBootMybatis&#xff0c;前端使用了Thymeleaf框架&#xff0c;数据库使用的是MySql 8.0。开发工具使用的是IDEA。 本系统前端是使用了前辈的管理系统模板&#xff0c;具体的系统模块功能如下图所示&#xff1a; 一、系统首页…

chatgpt赋能python:Python如何删除之前的内容

Python如何删除之前的内容 在Python编程中&#xff0c;删除之前输入或者生成的内容是一个常见的需求。本文将介绍如何在Python中删除之前的内容以及相关的技巧和方法。 为什么需要删除之前的内容&#xff1f; 在Python编程中&#xff0c;我们有时需要重新输入命令或代码段&a…

OpenGl光照之材质

文章目录 设置材质光的属性完整代码 在现实世界里&#xff0c;每个物体会对光产生不同的反应。比如&#xff0c;钢制物体看起来通常会比陶土花瓶更闪闪发光&#xff0c;一个木头箱子也不会与一个钢制箱子反射同样程度的光。有些物体反射光的时候不会有太多的散射(Scatter)&…

6月10日,今日信息差

1、中国科学家实现含氯废塑料高效无害升级回收。近日&#xff0c;中国科学院上海硅酸盐研究所首席研究员黄富强团队采用新型常温脱氯法&#xff0c;将含氯废塑料直接转化成多种高附加值新材料&#xff0c;成功实现高效无害升级回收&#xff0c;可广泛应用于绿色环保、新型储能、…

Linux5.2 LVS+keepalived高可用群集

文章目录 计算机系统5G云计算第三章 LINUX LVSKeepalived群集一、Keepalived 概述1. Keepalived 作用2.Keepalived 实现原理剖析3.VRRP协议&#xff08;虚拟路由冗余协议&#xff09;4.Keepalived 主要模块及其作用5.健康检查方式&#xff08;学名&#xff1a;探针&#xff09;…

chatgpt赋能python:Python怎么删库:谨慎使用

Python怎么删库&#xff1a;谨慎使用 Python是一种强大的编程语言&#xff0c;它被广泛用于各种项目中&#xff0c;不仅仅是数据科学和机器学习。但它也可以被用来执行危险的任务&#xff0c;比如删库。当你需要在Python中进行数据库操作时&#xff0c;一定要特别小心。在这篇…

Stable-Diffusion|文生图 拍立得纪实风格的Lora 图例(三)

上篇【Stable-Diffusion|入门怎么下载与使用civitai网站的模型&#xff08;二&#xff09;】介绍了如何使用c站进行文生图&#xff0c;尤其一些Lora可能随时会下架&#xff0c;所以及时测试&#xff0c;及时保存很关键&#xff0c;更新一些笔者目前尝试比较有意思的Lora。 本篇…

【python】【excel】在UI中加载EXCEL并修改

界面 代码 import tkinter as tk from tkinter import filedialog from pandastable import Table import pandas as pd import pyperclipclass ExcelEditor(tk.Frame):def __init__(self, parentNone):tk.Frame.__init__(self, parent)self.parent parentself.grid()self.cr…

chatgpt赋能python:如何在Python中添加空行?

如何在Python中添加空行&#xff1f; 如果你是一个有经验的Python工程师&#xff0c;在编写代码时你可能会遇到需要添加空行的情况。但是有几种方法可以实现这一点&#xff0c;你应该用哪种方法呢&#xff1f;在本文中&#xff0c;我们将探讨如何在Python中添加空行以及各种添…

TypeScript 5.1发布,新功能更新

文章目录 1&#xff1a;返回类型增加undefined2&#xff1a;getter可以设置和 setter 的不相关类型3&#xff1a;对 JSX 元素和 JSX 标签的异步支持4&#xff1a;支持命名空间属性名称 JSX5&#xff1a;typeRoots在模块更新6&#xff1a;JSX 标签的链接游标7&#xff1a;param …

Python中对文件的基本操作

文章目录 文件和目录路径文件的读取、写入、复制、删除、变更位置及修改名称解压缩zip格式的文件剪切板的应用使用python-docx处理Word文档使用openpyxl处理Excel文档示例&#xff1a;获取Excel文档中的数据生成Word文档 文件和目录路径 os库是Python内置的标准库&#xff0c;…

张天禹移动端学习

文章目录 相关概念&#xff08;一&#xff09;屏幕相关1. 屏幕大小2. 屏幕分辨率3. 屏幕密度 &#xff08;二&#xff09;像素相关1.物理像素2. css 像素3. 设备独立像素4.像素比5.像素之间的关系 &#xff08;三&#xff09;图片高清显示位图像素图片的高清显示&#xff08;媒…

chatgpt赋能python:Python下如何给网页添加背景图片

Python下如何给网页添加背景图片 随着现代互联网的快速发展&#xff0c;人们对于网页设计的要求越来越高&#xff0c;其中非常重要的一项就是背景图。在Python编程中&#xff0c;我们也可以很容易的为网页添加背景图片。 HTML中的background属性 要给网页加上背景图&#xf…

chatgpt赋能python:Python加法表达式,快速简便的计算方式

Python加法表达式&#xff0c;快速简便的计算方式 介绍 Python是一种可读性强、简洁、易于学习的编程语言&#xff0c;同时也是一种高级编程语言&#xff0c;由于其简洁性和可读性&#xff0c;越来越多的程序员们选择Python作为他们的工作语言。在Python中&#xff0c;加法表…

C++技能 - 详解使用Lambda表达式【再也不怕看不懂别人的代码了,干货还是蛮多的】

系列文章目录 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 C技能系列 期待你的关注哦&#xff01;&#xff01;&#xff01; 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the future of dream w…

C语言之函数初阶(2)

目录 1. 函数是什么 2. 库函数 3. 自定义函数 4. 函数参数 5. 函数调用 6. 函数的嵌套调用和链式访问 7. 函数的声明和定义 8. 函数递归 上一篇博客我们讲解了函数的前六个比较容易理解的部分&#xff0c;这一篇博客我们来讲解最后两个部分 在讲这篇博客之前&am…

Shell免交互操作

目录 一、Here Document 免交互 1.免交互定义 2.格式和使用方法 二、Expect 免交互 1.简介 2.格式和使用 &#xff08;1&#xff09;声明解释器 &#xff08;2&#xff09;spawn&#xff08;跟踪&#xff09; &#xff08;3&#xff09;expect&#xff08;期望&#x…