• Spring MVC -- JSP标准标签库(JSTL)


    JSP标准标签库(JavaServer Pages Standard Tag Library,JSTL)是一个JSP标签集合,它封装了JSP应用的通用核心功能。JSTL支持通用的、结构化的任务,比如迭代,条件判断,XML文档操作,国际化标签,SQL标签。 除了这些,它还提供了一个框架来使用集成JSTL的自定义标签。

    引入JSTL主要有以下两个优点:

    • 可以消除JSP页面中嵌入的JSP脚本,JSTL与EL相结合,会更加方便以及美观;
    • 各套框架(struts,SpringMVC等)都有自己的标签库,比如之前博客介绍到的SpringMVC中的表单标签库,这时JSTL可以作为公共、通用的,横行于各框架中。

    本篇博客要介绍的JSTL中最重要的标签,尤其是访问有界对象(pageScope、requestScope、sessionScope、applicationScope)、遍历集合、以及格式化数字和日期的那些标签。如果有兴趣进一步了解,可以在JSTL规范文档中找到所有JSTL标签的完整版说明。

    注意:随着EL 3.0的发布,所有的JSTL核心标记都可以用EL表达式替代。然而,有些旧项目中包含JSTL,因此掌握JSTL仍然是很有必要的。

    一 下载JSTL

    JSTL目前的最新版本是1.2,这是由JSR-52专家组在JCP(www.jcp.org)上定义的,JSTL库可以在以下网站下载:

    点击Download,打开如下页面:

    我们只需要下载前两个即可:

    • taglibs-standard-impl-1.2.5.jar:JSTL 1.2规范的实现包;
    • taglibs-standard-spec-1.2.5.jar:JSTL  1.2 API,包含了JSTL规范中定义的类型;

    剩下两个包:taglibs-standard-jstlel-1.2.5.jar和taglibs-standard-compat-1.2.5.jar都是为了兼容旧项目,这两个包都是JSTL 1.0规范的实现包,这里不做过多讨论。

    二 JSTL库 

    JSTL是标准标签库,但是它是通过多个标签库来暴露其行为的。根据JSTL标签所提供的功能,可以将其分为5个类别:

    区域 子函数 URI 前缀
    核心 变量支持 http://java.sun.com/jsp/jstl/core c
    流控制
    URL管理
    其他
    XML 核心 http://java.sun.com/jsp/jstl/xml x
    流控制
    转换
    国际化 语言区域 http://java.sun.com/jsp/jstl/fmt fmt
    消息格式化
    数字和日期格式化
    数据库 SQL http://java.sun.com/jsp/jstl/sql sql
    函数 集合长度 http://java.sun.com/jsp/jstl/functions fn
    字符串操作

    在JSP页面中使用JSTL库,必须通过以下格式使用taglib指令:

    <%@ taglib uri="uri" prefix="prefix" %>

    例如,要使用Core库,必须在JSP页面的开头处做以下声明:

    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

    这个前缀可以使任意的,但是采用惯例能使团队的其他开发人员以及后续加入该项目的其他人员更容易熟悉这些代码,因此,建议使用预订的前缀。

    三 核心标签

    核心标签是最常用的 JSTL标签。引用核心标签库的语法如下:

    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    标签描述
    <c:out> 用于在JSP中显示数据,就像<%= ... >
    <c:set> 用于保存数据
    <c:remove> 用于删除数据
    <c:catch> 用来处理产生错误的异常状况,并且将错误信息储存起来
    <c:if> 与我们在一般程序中用的if一样
    <c:choose> 本身只当做<c:when>和<c:otherwise>的父标签
    <c:when> <c:choose>的子标签,用来判断条件是否成立
    <c:otherwise> <c:choose>的子标签,接在<c:when>标签后,当<c:when>标签判断为false时被执行
    <c:import> 检索一个绝对或相对 URL,然后将其内容暴露给页面
    <c:forEach> 基础迭代标签,接受多种集合类型
    <c:forTokens> 根据指定的分隔符来分隔内容并迭代输出
    <c:param> 用来给包含或重定向的页面传递参数
    <c:redirect> 重定向至一个新的URL.
    <c:url> 使用可选的查询参数来创造一个URL

    1、out标签

    下面首先介绍Core库中用来操作有界变量的一般行为:out、set、remove。

    在运算表达式时,out标签是将结果输出到当前的JspWriter。out语法有两种形式,即有body content和没有body content。

        <c:out value="value" [escapeXml="true"] [default="defaultValue"]/>    
        <c:out value="value" [escapeXml="true"] >
            defaultValue
        </c:out>

    注意:在标签的语法中,[]是可选的属性。

    out标签的属性如下:

    属性 类型 描述
    value 对象 要计算的表达式
    escapeXml 布尔 当设置为true时,将value中的值以字符串的形式原封不动的显示出来;当设置为false,将value中的html标签以HTML格式显示;默认是true
    default 对象 默认值,当赋予给value属性的EL表达式返回null时,就会使用该默认值。

    例如,下列的out标签将输出有界变量x的值:

    <c:out value="${x}"/>

    其中x为字符串类型,值为“测试”。则在页面显示结果为:

    当把escapeXml设置false时,out会将字符实体码&lt;&gt;&#039;&#034;&amp转换成html对应的字符<、>、'、“和&。

        <c:out value="&lt;&gt;&#039;&#034;&amp;"  escapeXml="false"  /><br>
        <c:out value="&lt;&gt;&#039;&#034;&amp;"  escapeXml="true"  /><br>    

    输出如下:

    2、url标签

    url标签是非常有用的,简而言之,url标签执行以下任意操作:

    • 如果当前上下文路径为"/"(即应用程序部署为默认上下文“/”,“/”表示:服务器根路径),则它将空字符串附加到指定的路径;
    • 如果当前上下文路径不是“/”,它会将上下文路径添加到指定的路径。

    本节将会通过一个小的应用程序来解释url标签的重要性,应用程序的结构如下图:

    该应用由两个JSP页面main.jsp和admin.jsp组成。main.jsp文件位于应用程序根目录中,admin.jsp位于admin文件夹中。二者都需要显示在图像文件夹中的两个图像,image1.png,image2.png。请注意,图片的绝对路径是:

    http://host/context/image/image1.png
    http://host/context/image/image2.png

    因为两个图片从不同的位置被引用多次,为了方便使用,用一个包含文件来引用它们。任何需要显示图像的JSP页面仅需要将包含文件添加到文件中即可。

    (1)inc1.jsp:

    inc1.jsp
    <img src="image/image1.png"/>
    <img src="../image/image2.png"/>

    第一个包含文件包含路径是相对当前页的路径,假设main.jsp页面的URL是http://host/context/main.jsp,那么这两个图像的URL将被解析为以下URL:

    http://host/context/image/image1.png
    http://host/context/../image/image2.png

    不难想象,结果不太令人满意,第一个图像能正常显示,但是第二个图像不能。

    当通过http://host/context/admin/admin.jsp访问管理页面时,图像的URL解析成如下:

    http://host/context/admin/image/image1.png
    http://host/context/admin/../image/image2.png

    结果第一个图像不能正常显示,但是第二个图像将显示。

    很明显,使用相对路径并不能总工作,因为可以从不同的目录中的JSP页面调用包含文件。我们唯一希望的是使图像URL相对于应用程序本身,所以就有了第二个包含文件inc2.jsp。

    (2) inc2.jsp:

    inc2.jsp
    <img src="/image/image1.png"/>
    <img src="/image/image2.png"/>

    这样看起来不错,应该能工作了吧?遗憾的是,它并不适用于所有情况。这是因为在开发应用程序时,部署上下文路径通常是未知的。根据应用程序是否部署为默认上下文,admin.jsp页面可能具有以下URL之一:

    http://host/context/admin/admin.jsp
    http://host/admin/admin.jsp

    在这两种情况下,浏览器不知道上下文路径。事实上,在第一个URL的情况下,它会认为应用程序被部署为默认上下文(即服务器根路径),context是一个目录。因此,它将解析到第一个图像的URL为:

    http://host/image/image1.png

    很显然图像无法正常工作。实际上,仅当应用程序真正被部署到默认上下文时(即admin.jsp的URL为http://host/admin/admin.jsp),才能正确显示这两个图像。

    注意:<img src="/image/image1.png"/>这个根路径并不是指的应用程序的根路径,而是服务器根路径,或者说是http://localhost:port/;

    (2) inc3.jsp:

    <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
    inc3.jsp
    <img src="<c:url value="/image/image1.png"/>"/>
    <img src="<c:url value="/image/image2.png"/>"/>

    这种写法解决了我们的问题,因为url标签在服务器上执行,它知道上下文路径是什么。所以,它可以正确的解析图像的路径。这里的"/"指的就是应用程序的根路径

    (4) inc4.jsp:

    inc4.jsp
    <!-- 通过EL定义cp变量,但是不在html中显示 -->
    <!-- ${cp=pageContext.request.contextPath} -->
    <img src="${cp=="/"? "" : cp}/image/image1.png"/>
    <img src="${cp=="/"? "" : cp}/image/image2.png"/>
    <br/>
    ${cp}

    下面这种采用EL表达式的写法,我们使用以下EL表达式获取上下文路径:

    ${pageContext.request.contextPath} 

    相同的表达式将会被多次使用,因此创建了一个变量:

    ${cp=pageContext.request.contextPath}

    然而,该值仍然会发送到浏览器中显示,所以需要将其放在一个HTML注释中。然后,只需要测试上下文路径是"/"还是别的东西。

    ${cp=="/"? "" : cp}

    main.jsp:

    <!DOCTYPE html>
    <html>
        <head>
            <title>Main Page</title>
            <style>
                img {
                    width:200px;
                }
            </style>
        </head>
        <body>
            <h2>Main Page</h2>
            <%@include file="include/inc1.jsp"%>
            <hr/>
            <%@include file="include/inc2.jsp"%>
            <hr/>
            <%@include file="include/inc3.jsp"%>                
            <hr/>
            <%@include file="include/inc4.jsp"%>                
            
        </body>
    </html>

    admin.jsp:

    <!DOCTYPE html>
    <html>
        <head>
            <title>Admin</title>
            <style>
                img {
                    width:200px;
                }
            </style>
        </head>
        <body>
            <h2>Admin</h2>
            <%@include file="../include/inc1.jsp"%>
            <hr/>
            <%@include file="../include/inc2.jsp"%>                
            <hr/>
            <%@include file="../include/inc3.jsp"%>                
            <hr/>
            <%@include file="../include/inc4.jsp"%>                
        </body>
    </html>

    下面显示了非默认上下文中的admin.jsp页面:

    可以看到上下文路径是:/jstl-demo。

    3、set标签

    利用set标签,可以完成以下工作:

    • 创建一个字符串和一个引用该字符串的有界变量;
    • 创建一个引用现存有界对象的有界变量;
    • 设置有界对象的属性;

    注意:有界对象指的是隐式对象pageScope、requestScope、sessionScope、applicationScope。

    如果用set创建有界变量,那么在该标签出现后的整个JSP页面中都可以使用该变量。

    set便签的语法有4种形式。

    (1)第一种像是用于创建一个有界变量,并用value属性在其中定义一个要创建的字符串或者现存有界对象。

    <c:set value="value" var="varName" [scope="{page|request|session|application}"]/>

    这里的scope属性指定了有界变量的范围,默认变量范围是page。

    例如,下面的set标签则创建了字符串"The wisest fool",并将其赋给新创建的页面范围变量foo:

     <c:set var="foo" value="The wisest fool"/>
     ${foo}    

    输出如下:

    下面的set标签则创建了一个名为job的有界变量,它引用requestScope对象的position属性,变量job的范围为page:

     <c:set var="job" value="${requestScope.position}" scope="page"/>

    (2)第二种形式与第一种形式相似,只是要创建的字符串或者要引用的有界对象是作为body content赋值的:

    <c:set  var="varName" [scope="{page|request|session|application}"]>
       body content
    </c:set>

    第二种形式允许在body content中有JSP代码。

    (3)第三种形式是设置有界对象的属性值。target属性用于指定有界对象,property属性用于指定有界对象的属性。对该属性的赋值是通过value属性进行的:

    <c:set target="target" property="propertyName" value="value"/>

    例如,下面的set标签是将字符串"Tokyo"赋予有界对象address的city属性:

     <c:set target="${address}" property="city" value="Tokyo"/>    
     ${address.city}

    输出如下:

    注意:必须在target属性中用一个EL表达式来引用这个有界对象。

    (4)第4种方式与第三种形式相似,只是赋值是作为body content完成的:

    <c:set target="target" property="propertyName">
      body content
    </c:set>

    例如,下面的set标签是将字符串"Beijing"赋予有界对象address的city属性:

    <c:set target="${address}" property="city">
      Beijing
    </c:set>

    set标签的属性见表:

    属性 类型 描述
    value 对象 要创建的字符串,或者要引用的有界对象,或者新的属性值
    var 字符串 要创建的有界变量
    scope 字符串 新创建的有界变量的范围
    target 对象 其属性要被赋予新值的有界对象;这必须是一个JavaBeans实例或者Java.util.Map对象
    property 字符串 target所指定对象,要被赋予新值的属性名称

    4、remove标签

    remove标签用于删除有界对象,其语法如下:

    <c:remove var="varName" [scope="{page|request|session|application}"]/>

    注意:有界对象引用的对象不能删除,因此,如果另一个有界对象也引用了同一个对象,仍然可以通过另一个有界变量访问该对象。

    remove标签的属性见表:

    属性 类型 描述
    var 字符串 要删除的有界变量的名称
    scope 字符串 要删除的有界变量的范围,默认是page

    例如,下面的remove标签删除了页面范围的变量job:

     <c:remove var="job" scope="page"/>

    下面介绍Core库中执行条件行为的标签。JSTL中执行条件行为的标签有4个,即if、choose、when、otherwise。

    5、if标签

    if标签是对某一个条件进行测试,假设结果为true,就处理它的body content。测试结果保存在Boolean对象中,并创建有界变量来引用这个Boolean对象,利用var属性和scope属性分别定义有界变量的名称和范围。

     if的语法有两种形式。第一种形式没有body content。

    <c:if test="testCondition" var="varName" [scope="{page|request|session|application}"] />

    在这种情况下,var定义的有界对象一般是由其他标签在同一个JSP的后续阶段进行测试。

    第二种形式是使用一个body content:

        <c:if test="testCondition" [var="varName"] [scope="{page|request|session|application}"]>
            body content
        </c:if>

    body content是JSP,当测试条件的结果为true时,就会得到处理。例如:

        <c:if test="${param.user='ken' && param.password == 'blackcomb'}" scope="page">
            You logged in successfully.
        </c:if>

    if标签的属性见表:

    属性 类型 描述
    test 布尔 决定是否处理任何现有body content的测试条件
    var 字符串 引用测试条件值的有界变量名称;var的类型是Boolean
    scope 字符串 var定义的有界变量的范围

    为了模拟else,下面使用了两个if标签,并使用了相反的条件。例如,如果user和password参数的值为"ken"和“blackcomb”,以下代码片段将显示 "You logged in successfully.",否则,将显示"Login failed":

        <c:if test="${param.user='ken' && param.password == 'blackcomb'}" scope="page">
            You logged in successfully.
        </c:if>
        <c:if test="${!(param.user='ken' && param.password == 'blackcomb')}" scope="page">
            Login failed.
        </c:if>

    下面的if标签测试user和password参数值是否分别为“key”和"blackcomb",并将结果保存在页面范围的变量loggedIn中。之后,利用一个EL表达式,如果loggedIn变量值为true,则显示"You logged in successfully.",否则,将显示"Login failed":

       <c:if  var="loggedIn" test="${param.user='ken' && param.password == 'blackcomb'}"  scope="page">
        ...
       ${loggedIn ? "You logged in successfully.":"Login failed."}

    6、choose、when和otherwise标签

    choose和when标签的作用与Java中的关键字switch和case类似。也就是说,他们是用来为相互排斥的条件执行提供上下文的。choose标签中的必需嵌入有一个或者多个when标签,并且每个when标签都表示一种可以计算和处理的情况。otherwise标签则用于默认的条件快,假设没有任何一个when标签的测试条件结果为true,otherwise就会得到处理。假如是这种情况,otherwise就必须放在最后一个when之后。

    choose和otherwise标签没有属性,when标签必须带有定义测试条件的test属性,用来决定是否应该处理body content。

    举个例子,以下代码是测试参数status的值,如果status的值为full,将显示“You ara a full member.”;如果这个值是student,则显示“You are a student member.”;如果status参数不存在,或者它的值既不是full也不是student,那么这段代码将不显示任何内容:

        <c:choose>
            <c:when test="${ param.status=='full'}">
                You are a full member.
            </c:when> 
            <c:when test="${ param.status=='sdtudent'}">
                You are a student member.
            </c:when> 
        </c:choose>

    下面的例子与前面的例子相似,但是它利用了otherwise标签,如果status参数不存在,或者它的值不是full或student,则显示“Please register.”:

        <c:choose>
            <c:when test="${ param.status=='full'}">
                You are a full member.
            </c:when> 
            <c:when test="${ param.status=='sdtudent'}">
                You are a student member.
            </c:when> 
            <c:otherwise>
                Please register.
            </c:otherwise>
        </c:choose>

    下面介绍Core库中执行遍历行为的标签。JSTL中执行遍历行为的标签有2个,forEach和forTakens。这些标签封装了Java中的for,while,do-while循环。

    7、forEach标签

    forEach标签会无数次的反复遍历body content或者集合对象。可以遍历的对象包括java.util.Collection和java.util.Map的所有实现,以及对象数组或者基本类型。也可以遍历java.util.Iterator和java.util.Enumeration,但不应该在多个行为中使用Iterator或者Enumeration,因为无法重置Iterator或者Enumeration。

    forEach标签的语法有两种形式。第一种形式是固定次数的重复body content:

        <c:forEach [var="varName"] begin="begin" end="end" step="step">
            body content
        </c:forEach>

    第二种形式用于遍历集合对象:

        <c:forEach items="collection" [var="varName"] [varStatus="varStatusName"] [begin="beagin"]  [end="end"] [step="step"]>
            body content
        </c:forEach>

    body content是JSP。

    如果要遍历Map,要分别利用key和value属性引用一个Map key和一个Map value,遍历Map的伪代码如下所示:

        <c:forEach var="mapItem" items="map" >
            ${mapItem.key}:${mapItem.value}<br/>
        </c:forEach>

    forEach属性见表:

    属性描述 类型  默认值
    items 要被遍历的集合 支持的任意类型
    begin 开始的元素(0=第一个元素,1=第二个元素) 整数 0
    end 最后一个元素(0=第一个元素,1=第二个元素) 整数 Last element
    step 每一次迭代的步长 整数 1
    var 引用遍历的当前项的有界变量名称 字符串
    varStatus 保存遍历状态的有界变量名称,类型值为javax.servlet.jsp.jstl.core.LoopTagStatus 字符串

    例如,下列的forEach标签将显示“1,2,3,4,5”:

        <c:forEach var="x" begin="1" end="5" step="1">
            ${x }
        </c:forEach>

    下面的forEach标签将遍历有界变量address的phones属性:

        <c:forEach var="phone" items="${address.phones}" >
            ${phone}<br/>
        </c:forEach>

    对于每次变量,forEach标签都会创建一个有界变量,变量名通过var属性定义。在本例中,有界变量名为phone。forEach标签中的EL表达式用于显示phone的值,这个有界变量只存在于开始和结束的forEach标签之间,一到结束的forEach标签前,它就会被删除。

    forEach标签有一个类型为javax.servlet.jsp.jstl.core.LoopTagStatus的变量varStatus,LoopTagStatus接口带有count属性,它返回当前遍历的“次数”。第一次遍历时,varStatus.count值为1,;第二次遍历时,varStatus.count值为2。依此类推,通过测试varStatus.count%2的余数,可以知道该标签正在处理的是偶数编号的元素,还是奇数编号的元素。

    8、forTakens标签

    forTakens标签可以指定的分隔符来分隔内容并迭代输出,相当于Java.util.StirngTokenizer类。,其语法如下:

        <c:forTokens items="stringOfTakens" [delims="delimiters"] [var="varName"] [varStatus="varStatusName"] [begin="beagin"]  [end="end"] [step="step"]>
            body content
        </c:forTokens>

    body content是JSP,forTakens标签的属性如下:

     属性描述类型 默认值 
    items 要被遍历的token字符串 支持的任意类型
    begin 开始的元素(0=第一个元素,1=第二个元素) 整数 0
    end 最后一个元素(0=第一个元素,1=第二个元素) 整数 Last element
    step 每一次迭代的步长 整数 1
    var 引用遍历的当前项的有界变量名称 字符串
    varStatus 保存遍历状态的有界变量名称,类型值为javax.servlet.jsp.jstl.core.LoopTagStatus 字符串
    delims 一组分隔符 字符串

    下面是一个forTakens示例:

        <c:forTokens var="item" items="Argentina,Brazil,Chile" delims=",">
            <c:out value="${item}" /><br/>
        </c:forTokens>

    当将以上forTakens黏贴到JSP中时,它将会产生以下结果:

    四 格式化标签

    JSTL格式化标签用来格式化并输出文本、日期、时间、数字。引用格式化标签库的语法如下:

    <%@ taglib prefix="fmt"  uri="http://java.sun.com/jsp/jstl/fmt" %>
    标签描述
    <fmt:formatNumber> 使用指定的格式或精度格式化数字
    <fmt:parseNumber> 解析一个代表着数字,货币或百分比的字符串
    <fmt:formatDate> 使用指定的风格或模式格式化日期和时间
    <fmt:parseDate> 解析一个代表着日期或时间的字符串
    <fmt:bundle> 绑定资源
    <fmt:setLocale> 指定地区
    <fmt:setBundle> 绑定资源
    <fmt:timeZone> 指定时区
    <fmt:setTimeZone> 指定时区
    <fmt:message> 显示资源配置文件信息
    <fmt:requestEncoding> 设置request的字符编码

    1、formatNumber标签

    formatNumber标签用于格式化数字。你可以根据需要,利用这个标签的各种属性来获取自己想要的格式。formatNumber的语法格式有以下两种形式,第一种没有body content:

    <fmt:formatNumber  value="numericValue" 
           [type="{number|currency|percent}"] 
           [pattern="customPattern"]
           [currencyCode="currencyCode"]
           [currencySymbol="currencySymbol"]
           [groupingUsed="{true|false}"]
           [maxIntegerDigits="maxIntegerDigits"]
           [minIntegerDigits="minIntegerDigits"]
           [maxFractionDigits="maxFractionDigits"]
           [minFractionDigits="minFractionDigits"]
           [var="varName"]
           [scope="{page|request|session|application}"]
    />

    第二种形式有body content:

    <fmt:formatNumber  [type="{number|currency|percent}"] 
           [pattern="customPattern"]
           [currencyCode="currencyCode"]
           [currencySymbol="currencySymbol"]
           [groupingUsed="{true|false}"]
           [maxIntegerDigits="maxIntegerDigits"]
           [minIntegerDigits="minIntegerDigits"]
           [maxFractionDigits="maxFractionDigits"]
           [minFractionDigits="minFractionDigits"]
           [var="varName"]
           [scope="{page|request|session|application}"]>
        numeric value to be formatted
    </fmt:formatNumber>

    body content是JSP,formatNumber标签的属性见下表:

    属性描述类型默认值
    value 要显示的数字 字符串或者数字
    type 说明该值是要被格式化成数字、货币还是百分比。属性值如下:number,currency,或 percent类型 字符串 number
    pattern 指定一个自定义的格式化模式用于输出 字符串
    currencyCode ISO 4217货币代码(当type="currency"时) 字符串 取决于默认区域
    currencySymbol 货币符号 (当type="currency"时) 字符串 取决于默认区域
    groupingUsed 说明输出结果中是否包含组分隔符 布尔 true
    maxIntegerDigits 规定输出结果中整数部分最多几位数字 整数
    minIntegerDigits 规定输出结果中整数部分最少几位数字 整数
    maxFractionDigits 规定输出结果中小数部分最多几位数字 整数
    minFractionDigits 规定输出结果中小数部分最少几位数字 整数
    var 将输出结果存储为字符串的有界变量名称 字符串
    scope var的作用域 字符串 page

    formatNumber标签的用途之一就是将数字格式化成货币。为此:

    • 当type属性为currency时,可以利用currencyCode属性来定义一个ISO 4217货币代码。部分ISO 4217货币代码见表:
    货币 ISO 4217货币代码 大单位名称 小单位名称
    加拿大元 CAD 加元
    人民币 CNY
    欧元 EUR 欧元
    日元 JPY 日元
    英镑 GBP 英镑 便士
    美元 USD 美元 美分
    • 如果type属性为percent或number,那么就可以使用其它几个格式化数字属性。maxIntegerDigits属性和minIntegerDigits属性允许指定整数的长度,若实际数字超过了maxIntegerDigits所指定的最大值,则数字将会被截断;minFractionalDigits属性和maxFractionalDigits属性允许指定小数点后的位数。若实际的数字超出了所指定的范围,则这个数字会被截断。数字分组可以用来在每三个数字中插入一个逗号,groupingIsUsed属性用来指定是否使用数字分组,当与minIntegerDigits属性一同使用时,就必须要很小心地来获取预期的结果了。

    pattern属性可以在对数字编码时包含指定的字符。接下来的表格中列出了这些字符:

    符号描述
    0 代表一位数字
    E 使用指数格式
    # 代表一位数字,若没有则显示 0,前导 0 和追尾 0 不显示。
    . 小数点
    , 数字分组分隔符
    ; 分隔格式
    - 使用默认负数前缀
    % 百分数
    ? 千分数
    ¤ 货币符号,使用实际的货币符号代替
    X 指定可以作为前缀或后缀的字符
    ' 在前缀或后缀中引用特殊字符

    formatNumber标签的用法见下表:

        <!-- formatNumber标签 -->
        <fmt:formatNumber value="12" type="number"/><br/>
        <fmt:formatNumber value="12" type="number" minIntegerDigits="3"/><br/>
        <fmt:formatNumber value="12" type="number" minFractionDigits="2"/><br/>
        <fmt:formatNumber value="123456.78" pattern=".000"/><br/>
        <fmt:formatNumber value="123456.78" pattern="#,#00.0#"/><br/>
        <fmt:formatNumber value="12" type="currency"/><br/>
        <fmt:formatNumber value="12" type="currency" currencyCode="GBP"/><br/>
        <fmt:formatNumber value="0.12" type="percent"/><br/>
        <fmt:formatNumber value="0.12" type="percent" minFractionDigits="2"/><br/>

    输出如下:

    2、formatDate格式

    formatDate便签用于格式化日期,其语法格式如下:

    <fmt:formatDate  value="date"
      [type="{time|date|both}"]
      [dateStyle="{default|short|medium|long|full}"]
      [timeStyle="{default|short|medium|long|full}"]
      [pattern="customPattern"]
      [timeZone="timeZone"]
      [var="varName"]
      [scope="{page|request|session|application}"]/>

    body content是JSP,formatDate标签的属性见下表:

    属性描述类型默认值
    value 要格式化的日期、时间、日期和时间 java.util.Date
    type 说明要格式化的是时间、日期、还是时间与日期部分都要格式化,属性值:date, time, both 字符串 date
    dateStyle 预定义日期的格式化样式,遵循java.text.DateFormat中定义的语法。属性值:full, long, medium, short, 或 default 字符串 default
    timeStyle 预定义时间的格式化样式,遵循java.text.DateFormat中定义的语法。属性值:full, long, medium, short, 或 default 字符串 default
    pattern 自定义格式化模式 字符串
    timeZone 定义用于显示时间的时区 字符串 默认时区
    var 将输出结果存储为字符串的有界变量名称 字符串或者java.util.TimeZone
    scope var的作用域 字符串 page

    formatDate标签的格式话模式见下表:

    代码描述实例

    G

    时代标志

    AD

    y

    不包含纪元的年份。如果不包含纪元的年份小于 10,则显示不具有前导零的年份。

    2002

    M

    月份数字。一位数的月份没有前导零。

    April & 04

    d

    月中的某一天。一位数的日期没有前导零。

    20

    h

    12 小时制的小时。一位数的小时数没有前导零。

    12

    H

    24 小时制的小时。一位数的小时数没有前导零。

    0

    m

    分钟。一位数的分钟数没有前导零。

    45

    s

    秒。一位数的秒数没有前导零。

    52

    S

    毫秒

    970

    E

    周几

    Tuesday

    D

    一年中的第几天

    180

    F

    一个月中的第几个周几

    2 (一个月中的第二个星期三)

    w

    一年中的第几周r

    27

    W

    一个月中的第几周

    2

    a

    a.m./p.m. 指示符

    PM

    k

    小时(12 小时制的小时)

    24

    K

    小时(24 小时制的小时)

    0

    z

    时区

    中部标准时间

    '

     

    转义文本

    ''

     

    单引号

    下列代码利用formatDate标签格式化有界变量now引用的java.util.Date对象:

        <!-- dormatDate标签 -->
        <%
            String str = "2008-08-08 20:08:08";
            String dateFormat = "yyyy-MM-dd HH:mm:ss";
            SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
            /*
             * Date parse(String str)
             * 将给定的字符串按照SimpleDateFormat指定
             * 的日期格式解析并转换为Date对象返回
             */
            Date now = (Date) sdf.parse(str);
            out.println(now+"<br/>");
            pageContext.setAttribute("now", now);
        %>          
        Default:<fmt:formatDate value="${now}"/><br/>
        Short:<fmt:formatDate value="${now}" dateStyle="short"/><br/>
        Medium:<fmt:formatDate value="${now}" dateStyle="medium"/><br/>
        Long:<fmt:formatDate value="${now}" dateStyle="Long"/><br/>
        Full:<fmt:formatDate value="${now}" dateStyle="Full"/><br/>

    输出如下:

    下面的formatDate标签用于格式化时间:

        Default:<fmt:formatDate type="time" value="${now}"/><br/>
        Short:<fmt:formatDate type="time" value="${now}" timeStyle="short"/><br/>
        Medium:<fmt:formatDate type="time" value="${now}" timeStyle="medium"/><br/>
        Long:<fmt:formatDate type="time" value="${now}" timeStyle="Long"/><br/>
        Full:<fmt:formatDate type="time" value="${now}" timeStyle="Full"/><br/>

    下面的formatDate标签用于格式化日期和时间:

        Default:<fmt:formatDate type="both" value="${now}"/><br/>
        Short:<fmt:formatDate type="both" value="${now}" dateStyle="short"  timeStyle="short"/><br/>
        Medium:<fmt:formatDate type="both" value="${now}" dateStyle="medium"  timeStyle="medium"/><br/>
        Long:<fmt:formatDate type="both" value="${now}" dateStyle="Long"  timeStyle="Long"/><br/>
        Full:<fmt:formatDate type="both" value="${now}" dateStyle="Full"  timeStyle="Full"/><br/>

    下面的formatDate标签用于格式化带时区的时间:

        Time zone CT:<fmt:formatDate type="time" value="${now}" timeZone="CT"/><br/>
        Time zone HST:<fmt:formatDate type="time" value="${now}" timeZone="HST"/><br/>

    下面的formatDate标签利用定制模式来格式化日期和时间:

        <fmt:formatDate type="both" value="${now}" pattern="dd.MM.yy"/><br/>
        <fmt:formatDate type="both" value="${now}" pattern="dd.MM.yyyy"/><br/>

    3、timeZone标签

    timeZone标签用于定义时区,使其body content中的时间信息按指定时区进行格式化或者解析。其语法如下:

    <fmt:timezone value="timeZone">
       body content
    </fmt:timeZone>

    body content是JSP,属性值可以是类型为String或者java.util.TimeZone的动态值。

    如果value属性为null或者empty,则使用GMT时区。

    下面的范例用timeZone标签格式化带时区的日期:

        <!-- timeZone标签 -->
        <fmt:timeZone value="GMT+1:00">
            <fmt:formatDate value="${now}" type="both" dateStyle="full" timeStyle="full"/><br/>    
        </fmt:timeZone>
        
        <fmt:timeZone value="HST">
            <fmt:formatDate value="${now}" type="both" dateStyle="full" timeStyle="full"/><br/>    
        </fmt:timeZone>
        
        <fmt:timeZone value="CST">
            <fmt:formatDate value="${now}" type="both" dateStyle="full" timeStyle="full"/><br/>    
        </fmt:timeZone>

    美国和加拿大时区的值见下表:

    缩写 全名 时区
    NST 纽芬兰标准时间 UTC-3:30
    NDT 纽芬兰夏时制 UTC-2:30
    AST 大西洋标准时间 UTC-4
    ADT 大西洋夏时制 UTC-3
    EST 东部标准时间 UTC-5
    EDT 东部夏时制 UTC-4
    ET 东部时间,如EST和EDT *
    CST 中部标准时间 UTC-6
    CDT 中部夏时制 UTC-5
    CT 中部时间,如CST和CDT *
    MST 山地标准时间 UTC-7
    MDT 山地夏时制 UTC-6
    MT 山地时间,如MST和MDT *
    PST 太平洋标准时间 UTC-8
    PDT 太平洋夏时制 UTC-7
    PT 太平洋时间,如PST和PDT *
    AKST 阿拉斯加标准时间 UTC-9
    AKDT 阿拉斯加夏时制 UTC-8
    HST 夏威夷标准时间 UTC-10

    4、setTimeZone标签

    setTimeZone标签用于将指定时区保存在一个有界变量或者时间配置变量中,setTimeZone的语法如下:

    <fmt:setTimeZone value="timeZone" [var="varName"]  [scope="{page|request|session|application}"]/>

    下表显示了setTimeZone标签的属性:

    属性描述类型默认值
    value 时区 字符串或者java.util.TimeZone
    var 保存类型为java.util.TimeZone的时区有界变量 字符串
    scope 变量的作用与 字符串 page

    下面演示一个具体的示例:

        <!-- setTimeZone标签 -->
        <c:set var="now" value="<%=new java.util.Date()%>" />
        <p>当前时区时间: <fmt:formatDate value="${now}" 
                 type="both" timeStyle="long" dateStyle="long" /></p>
        <p>修改为 GMT-8 时区:</p>
        <fmt:setTimeZone value="GMT-8" />
        <p>Date in Changed Zone: <fmt:formatDate value="${now}" 
                 type="both" timeStyle="long" dateStyle="long" />
         </p>

    5、parseNumber标签

    parseNumber标签用于将以字符串表示的数字、货币或者百分百解析成数字。其语法有两种形式,第一种没有body content:

    <fmt:parseNumber  value="numericValue"
      [type="{number|currency|percent}"]
      [pattern="customPattern"]
      [parseLocale="parseLocale"]
      [integerOnly="{true|false}"]
      [var="varName"]
      [scope="{page|request|session|application}"]
    />

    第二种形式有body content:

    <fmt:parseNumber  value="numericValue"
      [type="{number|currency|percent}"]
      [pattern="customPattern"]
      [parseLocale="parseLocale"]
      [integerOnly="{true|false}"]
      [var="varName"]
      [scope="{page|request|session|application}"]>
      body content
    </fmt:parseNumber>

    body content是JSP。parseNumber标签的属性见表:

    属性描述类型默认值
    value 要解析的字符串 字符串或数字
    type 说明该字符串是要被解析成数字、货币还是百分比 字符串 number
    parseLocale 定义locale,在解析操作期间将其默认格式化样式,或将pattern属性定义的样式应用其中 字符串或者java.util.Locale 默认区域
    integerOnly 是否只解析整型数(true)或浮点数(false) 布尔 false
    pattern 自定义格式化模式 字符串
    timeZone 定义用于显示时间的时区 字符串或者java.util.TimeZone 默认时区
    var 保存输出结果的有界变量名称 字符串
    scope var变量的作用域 字符串 page

    下面的parseNumber标签就是解析有界变量balance的值,并将结果保存在有界变量i中:

         <!-- parseNumber标签 -->
         <c:set var="balance" value="1250003.350" />
         <fmt:parseNumber var="i" type="number" value="${balance}" />
         <p>数字解析 (1) : <c:out value="${i}" /></p>
         <fmt:parseNumber var="i" integerOnly="true" type="number" value="${balance}" />
         <p>数字解析 (2) : <c:out value="${i}" /></p>

    6、parseDate标签

    parseDate标签以区分地域的格式解析以字符串表示的日期和时间。其语法有两种形式,第一种没有body content:

    <fmt:parseDate  value="dateString"
      [type="{time|date|both}"]
      [dateStyle="{default|short|medium|long|full}"]
      [timeStyle="{default|short|medium|long|full}"]
      [pattern="customPattern"]
      [timeZone="timeZone"]
      [parseLocale="parseLocale"]
      [var="varName"]
      [scope="{page|request|session|application}"]/>

    第二种形式有body content:

    <fmt:parseDate  value="dateString"
      [type="{time|date|both}"]
      [dateStyle="{default|short|medium|long|full}"]
      [timeStyle="{default|short|medium|long|full}"]
      [pattern="customPattern"]
      [timeZone="timeZone"]
      [parseLocale="parseLocale"]
      [var="varName"]
      [scope="{page|request|session|application}"]>
     date value to be parsed
    </fmt:parseDate>

    body content是JSP,parseDate标签的属性见下表:

    属性描述类型默认值
    value 要解析的字符串 字符串
    type 说明要被解析的字符串是否包含日期、时间或者二者均有。属性值:date, time, 或 both 字符串 date
    dateStyle 日期的格式化样式。属性值:full, long, medium, short, 或 default 字符串 default
    timeStyle 时间的格式化样式。属性值:full, long, medium, short, 或 default 字符串 default
    pattern 自定义格式化模式,决定要如何解析该字符串 字符串
    timeZone 定义时区,是日期字符串中的时间信息均根据它来解析 字符串或者java.util.TimeZone 默认时区
    parseLocale 定义locale,在解析操作期间将其默认格式化样式,或将pattern属性定义的样式应用其中 字符串或者java.util.Locale 默认区域
    var 保存输出结果的有界变量名称 字符串 显示在页面
    scope var变量的作用域 字符串 页面

    下面的parseDate标签用于解析有界变量now引用的日期字符串,并将得到的java.util.Date保存在一个页面范围的有界变量parsedEmpDate中:

         <!-- parseDate标签 -->
         <c:set var="now" value="20-10-2015" />
         <fmt:parseDate value="${now}" var="parsedEmpDate" pattern="dd-MM-yyyy" />
         <p>解析后的日期为: <c:out value="${parsedEmpDate}" /></p>

    五 函数

    JSTL定义了一套可以再EL表达式中使用的标准函数。这些函数都集中放在functtion标签库中。要使用这些函数,必须在JSP的最前面使用以下的taglib指令:

    <%@ taglib prefix="fn"  uri="http://java.sun.com/jsp/jstl/functions" %>

    调用函数时,要以下列各式使用一个EL:

    ${fn:functionName}

    这里的functionName是函数名。

    大部分都用于字符串操作。例如,length函数用于字符串和集合,并返回集合或者数组中的项数,或者返回一个字符串的字符数。

    函数描述
    fn:contains() 测试输入的字符串是否包含指定的子串
    fn:containsIgnoreCase() 测试输入的字符串是否包含指定的子串,大小写不敏感
    fn:endsWith() 测试输入的字符串是否以指定的后缀结尾
    fn:escapeXml() 跳过可以作为XML标记的字符
    fn:indexOf() 返回指定字符串在输入字符串中出现的位置
    fn:join() 将数组中的元素合成一个字符串然后输出
    fn:length() 返回字符串长度
    fn:replace() 将输入字符串中指定的位置替换为指定的字符串然后返回
    fn:split() 将字符串用指定的分隔符分隔然后组成一个子字符串数组并返回
    fn:startsWith() 测试输入字符串是否以指定的前缀开始
    fn:substring() 返回字符串的子集
    fn:substringAfter() 返回字符串在指定子串之后的子集
    fn:substringBefore() 返回字符串在指定子串之前的子集
    fn:toLowerCase() 将字符串中的字符转为小写
    fn:toUpperCase() 将字符串中的字符转为大写
    fn:trim() 移除首尾的空白符

    1、contains()函数

    contains()函数用于确定一个字符串是否包含指定的子字符串。如果字符串中包含了该子字符串,则返回true,否则返回false,其语法如下:

    contains(string,substring)

    例如,下面两个EL表达式都返回true:

        <!-- contains()函数 -->
        <c:set var="myString" value="Hello World"/>
        ${fn:contains(myString,"Hello")}<br/>
        ${fn:contains("Stella Cadente","Cadente")}<br/>

    2、containsIgnoreCase()函数

    containsIgnoreCase()函数用于确定一个字符串是否包含指定的子字符串,忽略大小写,该函数与contains()类似。,其语法如下:

    containsIgnoreCase(string,substring)

    例如,下面的EL表达式将返回true:

        <!-- containsIgnoreCase()函数 -->    
        ${fn:containsIgnoreCase("Stella Cadente","CADENTE")}<br/>

    3、endsWith()函数

    endsWith()函数用于确定一个字符串是否以指定后缀结尾。其返回值是一个Boolean,语法如下:

    endsWith(string,suffix)

    例如,下面的EL表达式将返回true:

        <!-- endsWith()函数 -->    
        ${fn:endsWith("Hello World","World")}<br/>

    4、escapeXml()函数

    escapeXml()函数忽略用于XML标记的字符。其语法如下:

    escapeXml(string)

    例如,下面的EL表达式:

        <!-- excapexml()函数 -->
        <c:set var="string1" value="This is first String."/>
        <c:set var="string2" value="This <abc>is second String.</abc>"/>
    
        <p>使用 escapeXml() 函数:</p>
        <p>string (1) : ${fn:escapeXml(string1)}</p>
        <p>string (2) : ${fn:escapeXml(string2)}</p>
    
        <p>不使用 escapeXml() 函数:</p>
        <p>string (1) : ${string1}</p>
        <p>string (2) : ${string2}</p>

    5、indexOf()函数

    indexOf()函数返回指定子字符串在某个字符串中第一次出现时的索引。如果没有找到指定的子字符串,则返回-1。其语法如下:

    indexOf(string,substring)

    例如,下面的EL表达式返回7:

        <!-- indexOf()函数 -->
        ${fn:indexOf("Stella Cadente","Cadente")} 

    6、join()函数

     join()函数将一个String数组中的所有元素合并成一个字符串,并用指定的分隔符分开,其语法如下:

    join(array,separator)

    如果这个数组为null,就会返回一个空字符串。

    例如,下面的EL表达式:

        <!-- join()函数 -->
        <c:set var="string1" value="www runoob com"/>
        <c:set var="string2" value="${fn:split(string1, ' ')}" />
        <c:set var="string3" value="${fn:join(string2, '-')}" />
    
        <p>字符串为 : ${string3}</p>

    7、length()函数

    length()函数用于返回集合中的项数,或者字符串中的字符数,其语法如下:

    length(input)

    例如,下面的EL表达式:

        <!-- length()函数 -->
        <c:set var="string1" value="This is first String."/>
        <c:set var="string2" value="This is second String." />
    
        <p>字符串长度 (1) : ${fn:length(string1)}</p>
        <p>字符串长度 (2) : ${fn:length(string2)}</p>

    8、replace()函数

    replace()函数将字符串string中出现的所有beforeSubstring都用afterSubstring转换,并将结果返回,其语法如下:

    replace(string,beforeSubstring,afterSubstring)

    例如,下面的EL表达式:

        <!-- replace()函数 -->
        <c:set var="string1" value="I am from google"/>
        <c:set var="string2" value="${fn:replace(string1, 'google', 'runoob')}" />
        <p>替换后的字符串 : ${string2}</p>

    9、split()函数

    split()函数用于将一个字符串分割成一个子字符串数组。它的作用与join()相反。例如下列代码分割字符串“my,world”,并将结果保存在有界变量split中,随后,利用forEach标签将split格式化成一个HTML表格中:

        <!-- split()函数 -->
        <c:set var="split" value="${fn:split('my,world',',')}"/>
        <table>
            <c:forEach var="sub" items="${split}">
                <tr>
                    <td>
                        ${sub}
                    </td>
                </tr>
            </c:forEach>
        </table>

    10、startsWith()函数

    startsWith()函数用于测试一个字符串是否以指定的前缀开头,如果是,返回true,否则返回false。其语法如下:

    startsWith(string,prefix)

    例如,下面的EL表达式:

        <!-- startsWith()函数 -->
        <c:set var="string" value="Runoob: I am from Runoob."/>
        <c:if test="${fn:startsWith(string, 'Google')}">
               <p>字符串以 Google 开头</p><br/>
        </c:if>        
        <c:if test="${fn:startsWith(string, 'Runoob')}">
               <p>字符串以 Runoob 开头</p>
        </c:if>

    11、substring()函数

    substring()函数用于返回一个从指定基于0的起始索引到指定基于0的终止索引的子字符串,其语法如下:

    substring(string,beginIndex,endIndex)

    例如,下面的EL表达式:

        <!-- substring()函数 -->
        <c:set var="string1" value="This is first String."/>
        <c:set var="string2" value="${fn:substring(string1, 5, 15)}" />
        <p>生成的子字符串为 : ${string2}</p>

    12、substringAfter()函数

     substringAfter()函数用于返回指定子字符串第一次出现后的字符串部分,其语法如下:

    substringAfter(string,substring)

    例如,下面的EL表达式:

        <!-- substringAfter()函数 -->
        <c:set var="string1" value="This is first String."/>
        <c:set var="string2" value="${fn:substringAfter(string1, 'is')}" />
        <p>生成的子字符串 : ${string2}</p>

    13、substringBefore()函数

      substringAfter()函数用于返回指定子字符串第一次出现前的字符串部分,其语法如下:

    substringBefore(string,substring)

    例如,下面的EL表达式:

        <!-- substringBefore()函数 -->
        <c:set var="string1" value="This is first String."/>
        <c:set var="string2" value="${fn:substringBefore(string1,'first')}" />
        <p>生成的子字符串 : ${string2}</p>

    14、toLowerCase()函数

    toLowerCase()函数将一个字符串转换成它的小写版本,其语法如下:

    toLowerCase(string)

    例如,下面的EL表达式:

        <!-- toLowerCase()函数 -->
        <c:set var="string1" value="I am from RUNOOB"/>
        <c:set var="string2" value="${fn:toLowerCase(string1)}" />
        <p>字符串为 : ${string2}</p>

    15、toUpperCase()函数

    toUpperCase()函数将一个字符串转换成它的大写版本,其语法如下:

    toUpperCase(string)

    例如,下面的EL表达式:

        <!-- toUpperCase()函数 -->
        <c:set var="string1" value="I am from RUNOOB"/>
        <c:set var="string2" value="${fn:toUpperCase(string1)}" />
        <p>字符串为 : ${string2}</p>

    16、trim()函数

    trim()函数用于删除一个字符串开头和结束的空白,其语法如下:

    trim(string)

    例如,下面的EL表达式:

        <!-- trim()函数 -->
        <c:set var="string1" value="I am from runoob         "/>
        <p>string1 长度 : ${fn:length(string1)}</p>
    
        <c:set var="string2" value="${fn:trim(string1)}" />
        <p>string2 长度 : ${fn:length(string2)}</p>
        <p>字符串为 : ${string2}</p>

    六 XML标签

    JSTL XML标签库提供了创建和操作XML文档的标签。引用XML标签库的语法如下:

    <%@ taglib prefix="x"  uri="http://java.sun.com/jsp/jstl/xml" %>

    在使用xml标签前,你必须将XML 和 XPath 的相关包拷贝至你的<Tomcat 安装目录>lib下:

    标签描述
    <x:out> 与<%= ... >,类似,不过只用于XPath表达式
    <x:parse> 解析 XML 数据
    <x:set> 设置XPath表达式
    <x:if> 判断XPath表达式,若为真,则执行本体中的内容,否则跳过本体
    <x:forEach> 迭代XML文档中的节点
    <x:choose> <x:when>和<x:otherwise>的父标签
    <x:when> <x:choose>的子标签,用来进行条件判断
    <x:otherwise> <x:choose>的子标签,当<x:when>判断为false时被执行
    <x:transform> 将XSL转换应用在XML文档中
    <x:param> 与<x:transform>共同使用,用于设置XSL样式表

    七 SQL标签

    JSTL SQL标签库提供了与关系型数据库(Oracle,MySQL,SQL Server等等)进行交互的标签。引用SQL标签库的语法如下:

    <%@ taglib prefix="sql"  uri="http://java.sun.com/jsp/jstl/sql" %>
    标签描述
    <sql:setDataSource> 指定数据源
    <sql:query> 运行SQL查询语句
    <sql:update> 运行SQL更新语句
    <sql:param> 将SQL语句中的参数设为指定值
    <sql:dateParam> 将SQL语句中的日期参数设为指定的java.util.Date 对象值
    <sql:transaction> 在共享数据库连接中提供嵌套的数据库行为元素,将所有语句以一个事务的形式来运行

    参考文章

    [1]JSP 标准标签库(JSTL)(推荐)

    [2]jsp引用JSTL核心标签库

    [3]Spring MVC学习指南

  • 相关阅读:
    poj3660 最短路/拓扑序
    poj1502 最短路
    poj3259 最短路判环
    poj1680 最短路判环
    一些自己常用的cdn
    bower
    vuejs点滴
    jquery的ajax
    jquery点滴
    githubpage+hexo构建自己的个人博客
  • 原文地址:https://www.cnblogs.com/zyly/p/10861116.html
Copyright © 2020-2023  润新知