设为首页 收藏本站
查看: 941|回复: 0

[经验分享] *关于httl开源Java模板的使用心得

[复制链接]

尚未签到

发表于 2017-6-22 09:20:49 | 显示全部楼层 |阅读模式

  1.简介
  HTTL (Hyper-Text Template Language) 是一个高性能的开源JAVA模板引擎, 适用于动态HTML页面输出, 可替代JSP页面, 指令和Velocity相似。
  2.模板语法
  HTTL语法尽可能符合HTML和JAVA开发者的直觉,指令和老牌的Velocity类似,但改进了Velocity中不符合直觉的地方。 只保留最基本的条件迭代控制指令,渲染过程不允许修改原始数据,防止模板引入过多业务逻辑。默认使用HTML注释语法,避免干扰原生HTML页面。


<!--#set(List<Book> books)-->
<html>
    <body>
        <!--#if(books)-->
        <table>
            <!--#for(Book book : books)-->
            <tr>
                <td>${book.title}</td>
            </tr>
            <!--#end-->
        </table>
        <!--#end-->
    </body>
</html>

  3.改进Velocity不符合直觉的地方:

  • 指令中的变量不用加$符,如:#if(a == b),而不像Velocity那样:#if($a == $b),加$有点废话,而且容易忘写。
  • ${x}当变量为null时,输出空白串,而不像Velocity那样:输出源码${x},如果用$!{x},感叹号容易忘记写。
  • 支持在输出时进行表达式计算,如:${i + 1},而不像Velocity那样:要先#set($j = $i + 1)到一个临时变量。
  • 采用更直观的方式,调用静态工具方法,如:${"a".toChar},而不像Velocity那样:${StringTool.toChar("a")}。
  4.基本语法
  和Velocity类似




<html>
    <body>
        #if(books)
        <table>
            #for(Book book : books)
            <tr>
                <td>${book.title}</td>
            </tr>
            #end
        </table>
        #end
    </body>
</html>
  5.注释语法
  指令两边可以套上HTML注释,以免干扰原生HTML页面。
  HTTL在解析时,将自动去除指令边上的HTML注释符。(缺省已开启过滤)




<html>
    <body>
        <!--#if(books)-->
        <table>
            <!--#for(Book book : books)-->
            <tr>
                <td>${book.title}</td>
            </tr>
            <!--#end-->
        </table>
        <!--#end-->
    </body>
</html>
  缺省使用HTML注释符:(缺省值,不用配置)




comment.left=<!--
comment.right=-->
  如果你用HTTL生成Java代码,你也可以改为:




comment.left=/*
comment.right=*/
  6.属性语法
  基于Html标签属性: (指令和表达式与注释语法相同)




<html>
    <body>
        <table if="books">
            <tr for="Book book : books">
                <td>${book.title}</td>
            </tr>
        </table>
    </body>
</html>
  需要配置:




template.filters+=httl.spi.filters.AttributeSyntaxFilter
  属性语法需要用到jericho包解析HTML标签:
  jericho-html-3.1.jar




<dependency>
    <groupId>net.htmlparser.jericho</groupId>
    <artifactId>jericho-html</artifactId>
    <version>3.1</version>
</dependency>
  如果属性和其它框架冲突,可以添加名称空间:




attribute.namespace=httl
  名称空间写法如:




<tr httl:for="book : books" />
  在没有标签的地方,你可以同时使用上面的注释语法。
  7.兼容语法
  HTTL提供兼容Velocity语法的支持,只需配置:(从1.0.8版本开始支持)




template.filters+=httl.spi.filters.VelocitySyntaxFilter
  在兼容模式下,你依然可以使用HTTL的标准语法,便于逐步迁移。
  8.指令
  HTTL只有 #set, #if, #else, #for, #break, #macro 六个指令,以及输出占位和注释转义,保持最小指令集,以后也不会增加指令。
  不识别的指令名,将以文本输出,比如HTML颜色:#FF00EE。
  如果指令名和文本相接,如:#elseTEXT,可用无参括号隔开,如:#if(x)AAA#else()BBB#end()CCC
  9.输出指令
${}过滤输出
  输出表达式的计算结果,并进行过滤,比如:过滤变量中的HTML标签。




格式:
${expression}

示例:
${user.name}
  注:HTTL缺省开启了EscapeXmlFilter,以防止HTML注入攻击,如果你需要更强的过滤,请自行实现Filter,并配置到value.filters。此处为运行时热点,请注意性能。
  如果输出变量的类型为Template,则缺省不过滤,比如:${include("foo.httl")}
$!{}不过滤输出
  原样输出表达式的计算结果,不进行任何过滤,通常用于输出HTML片段。




格式:
$!{expression}

示例:
$!{body}
  注意:使用不过滤输出,请确保内容是开发者确定的安全内容,而不是由用户提交的内容,以防止HTML注入攻击。
  10.变量指令
#set变量类型
  声明变量的类型,模板内部其它变量类型基于此类型推导。




格式:
#set(type name, type name)

示例:
#set(User user, List<Book> books)
  注:暂时只支持List和Map的一级泛型,多级泛型能解析,但不能推导。
  如果有全局的变量,可以用配置全局导入变量类型声明,就不需要在每个模板声明:




import.variables+=User loginUser
  缺省已导入:(可以在模板中直接使用)




import.variables=Context parent,Template super,Template this,Engine engine
  如果使用的是HTTL内置的MVC集成,在集成中已缺省导入一些相关的常用变量,
  如果未声明的变量,缺省为Object:(如果是直接输出变量,可不声明类型)




default.variable.type=java.lang.Object




${obj} // 直接输出,不取属性,不做运算,不需要声明类型
  如果你的系统中,大部分为String类型操作,你也可以改成缺省String类型:




default.variable.type=java.lang.String
  多次设置变量的类型,如果类型为父子关系,以子类型优先:




#set(ParentClass var1)
#set(ChildClass var1)
  不管先后顺序,都以子类型优先:




#set(ChildClass var1)
#set(ParentClass var1)
  如果是两个完全不同的类型,将报错:




#set(OriginClass var1)
#set(DiffrentClass var1)
#set变量赋值
  将表达式的计算结果存入变量中。




格式:
#set(name = expression)
#set(type name = expression)

#set(name := expression)
#set(type name := expression)

示例:
#set(price = book.price * book.discount)
#set(int price = book.price * book.discount)
  注意,为了简化模板的书写,#set的变量全模板有效,不限制在块指令内:




#if(xxx)
    #set(var ="value") // 变量全模板有效,而不是if块内有效
#end
${var} // 可以访问到if块内var的值
  不需要像Java那样:




#set(var = null)
#if(xxx)
    #set(var ="value")
#end
${var}
  类型的声明,同时可以用作强制转型,比如:




#set(Book book = bookentry.value)
  如果bookentry.value的类型丢失,上面的写法可以恢复book的类型。
  一个set指令可同时有多个类型声明或赋值,用逗号分隔,但类型声明和赋值要分开写,如:




#set(User user, List<Book> books)
#set(price = book.price, discount = book.discount)
  赋值会在生成局部变量的同时,写入当前Context中, 如果有include()子模板,在子模板中也可以读到该变量。
  如果你要在父模板中拿到include子模板中的变量,或者你想在模板渲染之后拿到变量, 因模板渲染完,它的Context已pop(),所以需在模板中将变量写到上级Conetxt才能在外面读到。 如果你需要用向上级Context中赋值,可以用“:=”赋值,它将变量写入Context.getParent()中, 比如:




#set(price := book.price * book.discount)
  你可以在模板渲染后,通过Context.getContext().get("price");拿到上面的变量。
  注意:




// 你可以把入参设成不可修改的Map,不会影响运行。
Map<String, Object> parameters = Collections.unmodifiableMap(parameters);

// 传入的parameters在渲染过程中总是不会被修改,
// 确保渲染过程无副作用,以及多次渲染的幂等性。
template.render(parameters, writer);

// 模板中的#set(price = x)变量是put到Context的临时集合中的。
Context.getContext().put("price", x);

// 先在#set临时集合中查找,再到原生传入的parameters中查找,然后到上一级Context查找。
Context.getContext().get("price");
  11.条件指令
#if条件
  如果条件表达式计算结果为真或非空,则输出指令所包含的块。
  注意:对于非Boolean值:非零数字,非空字串,非空集合,非null对象,为真。




格式:
#if(expression)

示例:
#if(user.role =="admin")
    ...
#end
#else条件否则
  如果前面的#if条件不为真,则输出#else指令所包含的块。
  注意:#else指令可以直接带条件,以减少#if条件的组合。




格式:
#else
#else(expression)

示例:
#if(user.role =="admin")
    ...
#else(user.role =="member")
    ...
#else
    ...
#end
12.循环指令
#for循环
  迭代表达式产生的集合,以集合中的每项值,重复输出指令所包含的块。




格式:
#for(name : expression)
#for(type name : expression)

示例:
#for(book : books)
    ...
    ${for.index} //当前循环次数
    ${for.size} //循环集合大小
    ${for.first} //是否为第一次
    ${for.last} //是否为最后一次
#end
  多级#for循环,可以用${for.parent.index}或${for.parent.parent.index}获取状态。
  类型的声明,同时可以用作强制转型,比如:




#for(Book book : booklist)
    ${book.title}
#end
  如果booklist的泛型丢失,上面的写法可以恢复book的类型。
  在迭代前,可以对集合做操作,如:




## 执行9次
#for(9)

## 字面序列集合,输出1到9的数字
#for(i : 1..9)

## 字面离散集合,输出10,20,30三个数字
#for(i : [10, 20, 30])

## 取一个非空集合迭代,如果books1不为空,则迭代books1,否则迭代books2
#for(book : books1 || books2)

## 集合相加后,再迭代
#for(book : books1 + books2)

## 集合排序后,再迭代
#for(book : books.sort)

## 递归迭代,比如Menu有一个getChildren()方法返回子列表:
#for(Menu menu : menus.recursive("getChildren"))
#break循环中断
  当条件表达式为真或非空时,中断当前迭代过程。
  注意:#break可以直接带条件参数:#break(i == j),不用写成:#if(i == j) #break #end




格式:
#break
#break(expression)

示例:
#for(book : books)
    ...
    #break(for.index == 10)
    ...
#end
#else循环否则
  如果前面的#for集合为空,则输出#else指令所包含的块。
  注意:#for指令可以直接和#else联合使用,可以减少#if不为空判断条件的书写。




格式:
#else
#else(expression)

示例:
#for(book : books)
    ...
#else
    ...
#end
  13.模板指令
#macro模板片段
  将指令块封装成可复用的模板片段,它可当作变量传递,可重复执行输出,可被继承覆盖。




格式:
#macro(name)
#macro(name(name, name))
#macro(name(type name, type name))

示例:
#macro(xxx)
    ...
#end

${xxx} 以变量执行宏
${xxx(arg1, arg2)} 以方法执行宏
  模板继承宏覆盖,参见:继承示例
  在宏定义的同时,可以在定义的位置同时输出宏,或将宏赋值给变量,以及定义宏的参数,以简化书写。




格式:
#macro($name)
#macro($name(name, name))
#macro($name(type name, type name))

#macro(var = name)
#macro(var = name(name, name))
#macro(var = name(type name, type name))

#macro($name => cache)
#macro($name(name, name) => cache)
#macro($name(type name, type name) => cache)

#macro(var = name => cache)
#macro(var = name(name, name) => cache)
#macro(var = name(type name, type name) => cache)

示例:
#macro($xxx)
    ...
#end

#macro(xxx = xxx)
    ...
#end
  组合语法等价关系:




#macro($xxx)
    ...
#end

等价于:(在宏定义的位置同时输出)

#macro(xxx)
    ...
#end
${xxx}




#macro(aaa = xxx)
    ...
#end

等价于:(在宏定义的同时执行,并将结果赋值给指定变量)

#macro(xxx)
    ...
#end
#set(aaa = xxx)




#macro($xxx => cache)
    ...
#end

等价于:(在宏定义的位置同时输出)

#macro(xxx)
    ...
#end
${cache(xxx)}




#macro(aaa = xxx => cache)
    ...
#end

等价于:(在宏定义的同时执行,并将结果赋值给指定变量)

#macro(xxx)
    ...
#end
#set(aaa = cache(xxx))
#break模板中断
  当条件表达式为真或非空时,中断当前模板或宏的执行。
  注意:不在#for指令中的#break,表示中断模板或宏的执行。




格式:
#break
#break(expression)

示例:
#break(debug)
  14.注释指令
##行注释
  隐藏行注释的内容,以换行符结束,用于注解过程,或屏蔽指令内容。




格式:
## line comment

示例:
## This is line comment
#**#块注释
  隐藏块注释内容,可包含换行符,用于注解过程,或屏蔽指令内容。




格式:
#* block comment *#

示例:
#*
   This is block comment
*#
  15.转义指令
#[]#不解析块
  原样输出模板内容,用于输出纯文本内容,或批量转义块中的特殊符。




格式:
#[ no parse block ]#

示例:
#[ This is no parse block: #if ${name} ]#
\#\$特殊符转义
  原样输出指令特殊符,用于输出纯文本内容。




格式:
\#, \$, \\

示例:
\#xxx
\${xxx}
\\${xxx}
  16.表达式
  基于Java表达式和扩展方法。
  支持Java所有表达式,以下只列出与Java不同的点:

  • 所有null值的操作均返回null,比如:${foo.bar.blabla},如果foo为null,后面所有的操作最终为null,而不会空指针。
  • 双等号"=="会被解析成equals()方法比较,而不是比内存地址。
  • 单双引号都将生成字符串:'a'或"a"都是String类型,如果要字面声明char,请使用反单引号:`a`为char类型。
  • 加号"+"数字优先,${1 +"2"}输出3,而不是12,字符串拼接尽量用${s1}${s2},而不是${s1 + s2}
  • Bean属性会解析成getter方法调用,${user.name}等价于${user.getName()}
  • 所有实现Comparable的对象都支持比较运算符,比如:#if(date1 < date2),可以比较日期的先后。
  • 所有对象都支持逻辑与或,分别返回空值或非空值,比如:${list1 || list2},如果list1不为空则返回list1,否则返回list2。
  • List和Map可以方括号取值,比如:list[0]等价于list.get(0),map["abc"]等价于map.get("abc")
  • 大写3L将生成java.lang.Long值,小写3l将生成long值。
  • 支持is操作符,与instanceof相同,来源C#。
  属性查找顺序,以${obj.foo}为例:(编译时决定,不影响性能)

  • 首先查找有没有导入obj类型的foo()静态方法
  • 再查找obj.getFoo()函数
  • 再查找obj.isFoo()函数
  • 再查找obj.foo()函数
  • 再查找obj.foo属性
  17.操作符表达式
集合操作符




${list[0]} 等价于:${list.get(0)}

${map.abc} 等价于:${map.get("abc")}

${map["a.b.c"]} 等价于:${map.get("a.b.c")}

序列生成: 1..3
比如:
#for(i : 1..10)
${i}
#end

List生成: [123,"abc", var]
比如:
#for(color : ["red","yellow","blue"])
${color}
#end

Map生成: ["xxx": 123,"yyy":"abc","zzz": var]
比如:(此Map保持声明时的顺序)
#for(entry : ["red":"#FF0000","yellow":"#00FF00"])
${entry.key} = ${entry.value}
#end

集合相加:list1 + list2
比如:
#for(item : list1 + list2)
${item}
#end
逻辑操作符




#if(object)
等价于:
#if(object != null)

#if(string)
等价于:
#if(string!= null && string.length() > 0)

#if(list)
等价于:
#if(list != null && list1.size() > 0)

#for(item : list1 || list2)
等价于:
#for(item : list1 != null && list1.size() > 0 ? list1 : list2)
日期操作符




date1 > date2
date1 >= date2
date1 < date2
date1 <= date2
  18.函数表达式
转型函数




obj.to("com.foo.Bar")
obj.toMap
num.toDate
str.toDate
str.toDate("yyyy-MM-dd HH:mm:ss")
str.toChar
str.toBoolean
str.toByte
str.toInt
str.toLong
str.toFloat
str.toDouble
str.toClass
str.toLocale
集合函数




数组和List一样可以用size方法获取大小
array.size
list.size
map.size

list.sort
#for(item : list.sort)
#end

list.toCycle
#set(colors = ["red","blue","green"].toCycle)
#for(item : list)
${colors.next}
#end
文件函数




继承模板,以当前模板中的宏,替换父模板中的同名宏,执行父模板,输出到当前位置。
${extends("/layout.httl")}
${extends("/layout.httl","UTF-8")}
${extends("../layout.httl")}
${extends("../layout.httl","UTF-8")}

包含模板,执行目标模板,输出到当前位置。
${include("/template.httl")}
${include("/template.httl","UTF-8")}
${include("/template.httl", ["arg":"value"])}
${include("../template.httl")}
${include("../template.httl","UTF-8")}

包含模板中的宏,执行目标宏,输出到当前位置。
${include("/template.httl#macro")}
${include("/template.httl#macro","UTF-8")}
${include("/template.httl#macro")}
${include("/template.httl#macro", ["arg":"value"])}

读取目标文件中的内容,输出到当前位置。
${read("/text.txt")}
${read("/text.txt","UTF-8")}
国际化函数




${"key".message} 或 ${message("key")}
在localized=true时,依次查找下面文件中的key配置:
basename_zh_CN.properties
basename_zh.properties
basename.properties

${include("template.httl")}
在localized=true时,依次查找以下文件是否存在:
template_zh_CN.httl
template_zh.httl
template.httl
  httl.properties相关配置:




# 国际化信息配置文件前缀,将从Loader中查找/WEB-INF/messages_zh_CN.properties
message.basename=/WEB-INF/messages

# 国际化信息格式,支持message和string
# 分别对应MessageFormat.format()和String.format()
message.format=message

# 用户可以直接用UTF-8文件保存国际化信息,而不需要ascii2native
message.encoding=UTF-8

# 开启国际化查找
localized=true

# 缺省区域信息
locale=zh_CN
格式化函数




num.format("###,##0")
num.format("###,##0.##")
date.format("yyyy-MM-dd")
date.format("yyyy-MM-dd HH:mm:ss")
转义函数




str.escapeXml
str.unescapeXml
str.escapeUrl
str.unescapeUrl
str.escapeString
str.unescapeString
str.escapeBase64
str.unescapeBase64
JSON函数




# 将对象转成JSON串
obj.encodeJson

# 将JSON串解析成Map对象
str.decodeJson.toMap

# 将JSON串解析成对象
str.decodeJson("com.foo.Bar").to("com.foo.Bar")
  缺省使用内置的解析器转码JSON串。
  如需使用fastjson进行转码,需配置:
  httl.properties:




json.codec=httl.spi.codecs.FastjsonCodec
  pom.xml:
  fastjson-1.1.25.jar




<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.1.25</version>
</dependency>
XML函数




# 将对象转成XML串
obj.encodeXml

# 将XML串解析成对象
str.decodeXml.to("com.foo.Bar")
  缺省使用java.beans.XMLEncoder进行转码。
  如需使用xstream进行转码,需配置:
  httl.properties:




xml.codec=httl.spi.codecs.XstreamCodec
  pom.xml:
  xstream-1.4.3.jar




<dependency>
    <groupId>com.thoughtworks.xstream</groupId>
    <artifactId>xstream</artifactId>
    <version>1.4.3</version>
</dependency>
摘要函数




# 生成MD5码
str.md5

# 生成SHA码
str.sha

# 生成指定类型的摘要码
str.digest("MD5")
str.digest("SHA")
命名转换函数




# 转成下划线分隔命名
str.toUnderlineName

# 转成驼峰分隔命名 (首字母小写)
str.toCamelName

# 转成大写分隔命名 (首字母大写)
str.toCapitalName
系统函数




# 当前时间
${now()}

# 随机数
${random()}

# 唯一码
uuid()
  20.Velocity对比
  如果你用过Velocity模板,可以查看以下对比,加深了解:
  <1>语法对比

  •   HTTL指令中的变量不加$符,只支持#if(x == y),不支持#if($x == $y),因为指令中没有加引号的字符串就是变量,和常规语言的语法一样,加$有点废话,而且容易忘写。
  •   HTTL占位符必需加大括号,只支持${aaa},不支持$aaa,因为$在JavaScript中也是合法变量名符号,而${}不是,减少混淆,也防止多人开发时,有人加大括号,有人不加,干脆没得选,都加,保持一致。
  •   HTTL占位符当变量为null时输出空白串,而不像Velocity那样原样输出指令原文,即${aaa},等价于Velocity的$!{aaa},以免开发人员忘写感叹号,泄漏表达式源码,如需原样输出,可使用转义\${aaa}, 在HTTL中,${aaa}表示不对内容进行过滤,用于原样输出HTML片段。
  •   HTTL支持在所有使用变量的地方,进行表达式计算,也就是你不需要像Velocity那样,先#set($j = $i + 1)到一个临时变量,再输出临时变量${j},HTTL可以直接输出${i + 1},其它指令也一样,比如:#if(i + 1 == n)。
  •   HTTL采用扩展Class原生方法的方式,如:${"a".toChar},而不像Velocity的Tool工具方法那样:$(StringTool.toChar("a")),这样的调用方式更直观,更符合代码书写习惯。
  <2>指令对比
VelocityHTTL异同功能变化
${xxx.yyy}
$xxx.yyy
${xxx.yyy}相同输出占位符HTTL大括号必需
$!{xxx.yyy}
$!xxx.yyy
$!{xxx.yyy}不同空值不显示源码VM为空值不显示源码
HTTL改为不过滤输出
## ...
#* ... *#
## ...
#* ... *#
相同不显示注释块
#[[ ... ]]##[ ... ]#相似不解析文本块HTTL少一对方括号
\# \$ \\\# \$ \\相同特殊符转义
#set($xxx = $yyy)#set(xxx = yyy)
#set(Type xxx = yyy)
#set(Type xxx)
相同给变量赋值HTTL可带类型声明
#if($xxx == $yyy)#if(xxx == yyy)相同条件判断
#elseif($xxx == $yyy)#else(xxx == yyy)相似否则条件判断HTTL复用#else指令
#else#else相同否则判断
#end#end
#endif
#end(if)
相同结束指令HTTL可带配对指令名
#foreach($item in $list)#for(item : list)
#for(Type item : list)
相似列表循环HTTL改为Java格式
#break#break
#break(xxx == yyy)
相同中断循环HTTL可以直接带条件
#stop#break
#break(xxx == yyy)
相似停止模板解析HTTL复用#break指令
#macro($xxx)#macro(xxx)不同可复用模板片段宏VM将宏作为指令执行
HTTL作为函数执行
#define($xxx)#macro(xxx = xxxmacro)相似捕获块输出到变量中HTTL复用#macro指令
#include("xxx.txt")${read("xxx.txt")}相似读取文本文件内容HTTL改为函数扩展
#parse("xxx.vm")${include("xxx.httl")}相似包含另一模板输出HTTL改为函数扩展
#evaluate("${1 + 2}")${render("${1 + 2}")}相似模板求值HTTL改为函数扩展

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-386724-1-1.html 上篇帖子: 前端 初级篇(HTML) 下篇帖子: (转) Learning from Imbalanced Classes
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表