為什麼要用到簡單標籤?
上一篇部落格中我已經講解了傳統標籤,想要開發自定義標籤,大多數情況下都要重寫doStartTag(),doAfterBody()和doEndTag()方法,並且還要知道SKIP_BODY,EVAL_BODY等等的變數代表著什麼,在什麼方法中使用。這樣實在是太麻煩了!
因此,為了簡化標籤開發的複雜度,在JSP 2.0中定義了一個更為簡單、便於編寫和呼叫的SimpleTag介面來實現標籤的功能。。
一般來說,實現了SimpeTag介面的標籤稱之為簡單標籤
SimpleTag介面
- 首先我們來看一下它的原始碼吧:
public interface SimpleTag extends JspTag {
void doTag() throws JspException, IOException;
void setParent(JspTag var1);
JspTag getParent();
void setJspContext(JspContext var1);
void setJspBody(JspFragment var1);
}
複製程式碼
- setParent()和getParent()方法就不多說了,我們來看一下剩下的3個方法:
void doTag() throws JspException, IOException;
void setJspContext(JspContext var1);
void setJspBody(JspFragment var1);
複製程式碼
- 明顯地:
- doTag()就是我們要寫程式碼處理邏輯地方
- setJspContext(JspContext var1)是將PageContext物件傳遞給標籤處理器類(PageContext是JspContext的子類)
- setJspBody(JspFragment var1)把代表標籤體的JspFragment物件傳遞給標籤處理器物件
快速入門
-
一般地,我們做開發都是繼承SimpleTagSupport類(該類實現了SimpleTag)來編寫自定義標籤
-
下面我們就來個快速入門吧:
-
目標:傳入字串格式就可以顯示想要的格式日期,對比之前傳統標籤的,看有什麼不同之處
-
標籤處理器類:
public class Demo1 extends SimpleTagSupport {
String format = null;
@Override
public void doTag() throws JspException, IOException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
this.getJspContext().getOut().write(simpleDateFormat.format(new Date()));
}
public String getFormat() {
return format;
}
public void setFormat(String format) {
this.format = format;
}
}
複製程式碼
- tld檔案:
<tag>
<name>formatDate</name>
<tag-class>tag.Demo1</tag-class>
<body-content>tagdependent</body-content>
<attribute>
<name>format</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
複製程式碼
- 效果:
- 簡單標籤的好處就是不用去理會doStartTag、doEndTag、SKIP_BODY以及一系列的方法和屬性!
- 簡單標籤一個doTag()方法走天下!
SimpleTagSupport類的執行順序:
- ①WEB容器呼叫標籤處理器物件的setJspContext方法,將代表JSP頁面的pageContext物件傳遞給標籤處理器物件
- ②WEB容器呼叫標籤處理器物件的setParent方法,將父標籤處理器物件傳遞給這個標籤處理器物件。【注意,只有在標籤存在父標籤的情況下,WEB容器才會呼叫這個方法】
- ③如果呼叫標籤時設定了屬性,容器將呼叫每個屬性對應的setter方法把屬性值傳遞給標籤處理器物件。如果標籤的屬性值是EL表示式或指令碼表示式,則WEB容器首先計算表示式的值,然後把值傳遞給標籤處理器物件。
- ④如果簡單標籤有標籤體,容器將呼叫setJspBody方法把代表標籤體的JspFragment物件傳遞進來
- ⑤執行標籤時:容器呼叫標籤處理器的doTag()方法,開發人員在方法體內通過操作JspFragment物件,就可以實現是否執行、迭代、修改標籤體的目的。
深入簡單標籤
在我們講解傳統標籤的時候,配合著SKIP_BODY、SKIP_PAGE等變數可以實現如下的功能:
- 控制jsp頁面某一部分內容(標籤體)是否執行。
- 控制整個jsp頁面是否執行。
- 控制jsp頁面內容重複執行。
- 修改jsp頁面內容輸出。
-
簡單標籤可沒有這些變數呀,那它怎麼才能實現上面那些功能呢?
-
在doTag方法中可以丟擲javax.servlet.jsp.SkipPageException異常,用於通知WEB容器不再執行JSP頁面中位於結束標記後面的內容,這等效於在傳統標籤的doEndTag方法中返回Tag.SKIP_PAGE常量的情況,我們來測試一下,在上面例子的程式碼中新增:
throw new SkipPageException();
複製程式碼
- 效果:
- 至於其他的功能下面會講到!
帶標籤體的簡單標籤
-
SimpleTagSupport也可以帶標籤體,但是處理方法和傳統標籤完全不同。
- 傳統標籤是這樣子的:將標籤體的內容通過setBodyContent()注入到BodyContent物件中。
- 簡單標籤是這樣子的:通過JspFragment物件實現!
-
我們來看一下JspFragment物件的原始碼吧:
public abstract class JspFragment {
public JspFragment() {
}
public abstract void invoke(Writer var1) throws JspException, IOException;
public abstract JspContext getJspContext();
}
複製程式碼
JspFragment物件十分簡單,重要的只有invoke(Writer var1)方法(獲取JspContext物件並不重要,在標籤描述器上就可以獲取到了)
public abstract void invoke(java.io.Writer out) :
- 用於執行JspFragment物件所代表的JSP程式碼片段
- 引數out用於指定將JspFragment物件的執行結果寫入到哪個輸出流物件中,如果傳遞給引數out的值為null,則將執行結果寫入到JspContext.getOut()方法返回的輸出流物件中。(簡而言之,可以理解為寫給瀏覽器)
- 下面是標籤處理器類的程式碼:
public class Demo1 extends SimpleTagSupport {
@Override
public void doTag() throws JspException, IOException {
//得到代表標籤體的物件
JspFragment jspFragment = getJspBody();
//invoke方法接收的是一個Writer,如果為null,就代表著JspWriter(),將標籤體的資料寫給瀏覽器!
jspFragment.invoke(null);
}
}
複製程式碼
- 效果:
-
既然標籤體的內容是通過JspFragment物件的invoke()方法寫給瀏覽器的,那麼那麼那麼,我只要控制好invoke()方法,我想幹什麼就幹什麼!
-
也就是說:
- 不呼叫invoke()方法,標籤體內容就不會輸出到瀏覽器上
- 重複呼叫invoke()方法,標籤體內容就會被重複執行
- 若想在標籤處理器中修改標籤體內容,只需在呼叫invoke方法時指定一個可取出結果資料的輸出流物件(例如StringWriter),讓標籤體的執行結果輸出到該輸出流物件中,然後從該輸出流物件中取出資料進行修改後再輸出到目標裝置,即可達到修改標籤體的目的
-
來來來,我們來試驗一下:
- 不呼叫invoke()方法
public void doTag() throws JspException, IOException { //得到代表標籤體的物件 JspFragment jspFragment = getJspBody(); //jspFragment.invoke(null); } 複製程式碼
-
標籤體的內容沒有輸出
-
呼叫兩次invoke()方法
public void doTag() throws JspException, IOException { //得到代表標籤體的物件 JspFragment jspFragment = getJspBody(); jspFragment.invoke(null); jspFragment.invoke(null); } 複製程式碼
- 標籤體的內容被輸出了兩次!
- invoke()方法指定別的輸出流(StringWriter),將標籤體的內容寫到流物件中,再通過流物件把資料取出來,達到修改的目的。
//得到代表標籤體的物件 JspFragment jspFragment = getJspBody(); //建立可以儲存字串的Writer物件 StringWriter stringWriter = new StringWriter(); //invoke()方法把標籤體的資料都寫給流物件中 jspFragment.invoke(stringWriter); //把流物件的資料取出來,流物件的資料就是標籤體的內容 String value = stringWriter.toString(); //將資料改成是大寫的,寫到瀏覽器中 getJspContext().getOut().write(value.toUpperCase()); 複製程式碼
- 標籤體的內容被改成了大寫!
我們可以發現,傳統標籤能完成的功能,簡單標籤都可以完成,並且更為簡單!
自定義標籤的應用
既然我們學了簡單標籤,我們就用簡單標籤來做開發吧!
防盜鏈
在講解request物件的時候,我們講解過怎麼實現防盜鏈的功能。現在我們使用標籤來進行防盜鏈!
模擬下場景:1.jsp頁面是海賊王資源,2.jsp頁面提示非法盜鏈,index1.jsp是我的首頁。別人想要看我的海賊王資源,就必須通過我的首頁點進去看,否則就是非法盜鏈!
- 標籤處理器的程式碼:
@Override
public void doTag() throws JspException, IOException {
//如果想要做成更加靈活的,就把站點設定和資源設定成標籤屬性傳遞進來!
//等會我們要獲取得到request物件,需要使用到JspContext的子類PageContext
PageContext pageContext = (PageContext) this.getJspContext();
//獲取request物件
HttpServletRequest httpServletRequest = (HttpServletRequest) pageContext.getRequest();
//獲取到referer
String referer = httpServletRequest.getHeader("Referer");
//獲取到response物件,等會如果是非法盜鏈,就重定向別的頁面上
HttpServletResponse httpServletResponse = (HttpServletResponse) pageContext.getResponse();
//非法盜鏈!
if (referer == null || !referer.startsWith("http://localhost:8080/zhongfucheng")) {
//2.jsp提示了非法盜鏈!
httpServletResponse.sendRedirect("/zhongfucheng/2.jsp");
//不執行頁面下面的內容了,保護頁面
throw new SkipPageException();
}
}
複製程式碼
- 1.jsp程式碼:
<zhongfucheng:Referer/>
<html>
<head>
<title></title>
</head>
<body>
海賊王最新資源
</body>
</html>
複製程式碼
- index1.jsp程式碼:
<h1>這是首頁!</h1>
<a href="${pageContext.request.contextPath}/1.jsp"> 海賊王最新資源</a>
複製程式碼
- 2.jsp程式碼:
<body>
你是非法盜鏈的!!!!!!
</body>
複製程式碼
- **第一次我是直接訪問1.jsp,Referer是為空的,所以是非法盜鏈。第二次我是通過從首頁點進去看的,所以可以訪問1.jsp。**效果圖:
if標籤
在JSTL中,我們已經使用過了<c:if/>
標籤了,現在我們學習了自定義標籤,可以開發類似於JSTL的if標籤了!
既然是if標籤,那麼就需要編寫帶屬性和帶標籤體的標籤(需要判斷是true還是false呀!,通過判斷是否為真值來決定是否執行標籤體的內容)
- 標籤處理器程式碼:
public class Demo1 extends SimpleTagSupport {
//定義一個Boolean型別的變數
boolean test ;
@Override
public void doTag() throws JspException, IOException {
//獲取到代表標籤體內容的物件
JspFragment jspFragment = this.getJspBody();
//如果為真值才執行標籤體的內容
if (test == true) {
jspFragment.invoke(null);
}
}
public boolean isTest() {
return test;
}
public void setTest(boolean test) {
this.test = test;
}
}
複製程式碼
- tld檔案的程式碼:
<tag>
<name>if</name>
<tag-class> tag.Demo1</tag-class>
<body-content>scriptless</body-content>
<attribute>
<name>test</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
複製程式碼
- 本來就沒有user這個域物件屬性,所以user就是為null:
- 將user改成不為null,瀏覽器就沒有輸出了
forEach標籤
forEach標籤最基本的功能:遍歷集合、陣列
- 首先,我先寫一個可以遍歷List集合的標籤,可能我們會這樣設計:
public class Demo2 extends SimpleTagSupport {
//遍歷的是List集合,於是標籤的屬性就為List
private List items;
//遍歷出來的物件就用Object存著,因為我們不知道List集合儲存的是什麼元素
private Object var;
@Override
public void doTag() throws JspException, IOException {
//獲取到迭代器
Iterator iterator = items.iterator();
//遍歷集合
while (iterator.hasNext()) {
//獲取到集合的元素
var = iterator.next();
//.....var屬性代表的就是集合的元素,現在問題來了,好像在標籤體內無法獲取到這個物件....
//做到這裡完成不下去了....
}
}
public void setItems(List items) {
this.items = items;
}
public void setVar(Object var) {
this.var = var;
}
}
複製程式碼
上面的思路是正常的,但是做不下去!我們換一個思路唄。上面的問題主要是在標籤體獲取不到被遍歷出來的物件!
我們這樣做:把var定義成String型別的,如果遍歷得到物件了,就設定PageContext的屬性,var為關鍵字,物件為值。在標籤體用EL表示式搜尋以var為關鍵字的物件!每遍歷出一個物件,就執行一次標籤體!
- 於是就有了以下的程式碼!
public class Demo1 extends SimpleTagSupport {
//遍歷的是List集合,定義List集合成員變數
private List items;
//以var為關鍵字儲存到PageContext
private String var;
@Override
public void doTag() throws JspException, IOException {
//獲取到集合的迭代器
Iterator iterator = items.iterator();
//獲取到代表標籤體內容的物件
JspFragment jspFragment = this.getJspBody();
//遍歷集合
while (iterator.hasNext()) {
Object o = iterator.next();
//把遍歷出來的物件儲存到page範圍中,關鍵字為標籤的屬性var(在標籤體中使用EL表示式${var},就能夠獲取到集合的物件了!)
this.getJspContext().setAttribute(var, o);
//每設定了一個屬性,我就執行標籤體
jspFragment.invoke(null);
}
}
public void setItems(List items) {
this.items = items;
}
public void setVar(String var) {
this.var = var;
}
}
複製程式碼
- tld檔案如下:
<tag>
<name>forEach</name>
<tag-class>tag.Demo1</tag-class>
<body-content>scriptless</body-content>
<attribute>
<name>var</name>
<rtexprvalue>true</rtexprvalue>
<!--字串即可,不需要EL表示式的支援-->
<required>false</required>
</attribute>
<attribute>
<name>items</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
複製程式碼
- 測試的jsp程式碼如下:
<%
List list = new ArrayList();
list.add("zhongfucneng");
list.add("1");
list.add("2");
list.add("3");
request.setAttribute("list",list);
%>
<zhongfucheng:forEach items="${list}" var="str">
${str}
</zhongfucheng:forEach>
複製程式碼
- 效果:
上面寫的僅僅能夠遍歷List集合,做一個通用的forEach標籤麻煩的是在:不知道傳進來的是什麼型別的陣列、什麼型別集合!,需要逐一去判斷
-
我們的實現思路就是將所有的集合或陣列都轉成是Collection型別的!
-
我們來嘗試著寫一下:
//如果items是Collection型別的,就強轉為Colletion
if (items instanceof Collection) {
collection = (Collection) items;
}
//如果itmes是Map型別的,那麼就強轉為Map,再獲取到<Map.Entry<K, V>,這個是Set集合的!
if (items instanceof Map) {
Map map = (Map) items;
collection = (Collection) map.entrySet();
}
//物件陣列
if (items instanceof Object[]) {
Object[] objects = (Object[]) items;
collection = Arrays.asList(objects);
}
//int[],Byte[],char[]等八大基本資料型別.....
複製程式碼
還有int[],byte[],char[]等八大基本資料型別,這八大基本資料型別就不能用Arrays.asList()把引用傳進去了。因為JDK5以後會把引用自動裝箱成Interger[]、Byte[]等等,而不是獲取到陣列的元素資料。
- 測試程式碼如下:
public static void main(String[] args) {
int[] ints = new int[]{1, 2, 3};
Object[] objects = new Object[]{"1", "2", "3"};
if (objects instanceof Object[]) {
Collection collection = Arrays.asList(objects);
System.out.println(collection);
}
if (ints instanceof int[]) {
Collection collection1 = Arrays.asList(ints);
System.out.println(collection1);
}
}
複製程式碼
- 效果:
- 對於八大基本型別資料我們就可以這樣幹:
if (items instanceof int[]) {
int[] ints = (int[]) items;
collection = new ArrayList();
for (int anInt : ints) {
collection.add(anInt);
}
}
//......這裡還要寫7個
複製程式碼
- JSTL的forEach標籤類似就是這樣乾的:
由於JDK5的新特性,我們又有另外的解決方案,Class物件能夠判斷是否為陣列類,reflect反射包下Array類:
-
其實,無論Map集合、還是任何型別的陣列、都可以使用Colletion進行遍歷!。
-
如果是八大基本資料型別的陣列,我們就使用反射來進行構建出Collection集合。
-
標籤處理器的程式碼:
public class Demo1 extends SimpleTagSupport {
//遍歷的是未知的集合或陣列,定義成Object
private Object items;
//每次被遍歷的物件儲存關鍵字
private String var;
//Colletion
private Collection collection;
//在WEB容器設定標籤的屬性的時候,判斷是什麼型別的陣列和集合
public void setItems(Object items) {
this.items = items;
//如果items是Collection型別的,就強轉為Colletion
if (items instanceof Collection) {
collection = (Collection) items;
}
//如果itmes是Map型別的,那麼就強轉為Map,再獲取到<Map.Entry<K, V>,這個是Set集合的!
if (items instanceof Map) {
Map map = (Map) items;
collection = (Collection) map.entrySet();
}
//可以這樣解決,Class物件判斷是否是一個陣列類
if (items.getClass().isArray()) {
//建立Collection集合新增陣列的元素!
collection = new ArrayList();
//再利用reflect包下的Array類獲取到該陣列類的長度
int len = Array.getLength(items);
//遍歷並新增到集合中
for (int i = 0; i < len; i++) {
collection.add(Array.get(items, i));
}
}
}
public void setVar(String var) {
this.var = var;
}
@Override
public void doTag() throws JspException, IOException {
//獲取到代表標籤體內容的物件
JspFragment jspFragment = this.getJspBody();
Iterator iterator = collection.iterator();
//遍歷集合
while (iterator.hasNext()) {
Object o = iterator.next();
//把遍歷出來的物件儲存到page範圍中(在標籤體中使用EL表示式${var},就能夠獲取到集合的物件了!)
this.getJspContext().setAttribute(var, o);
jspFragment.invoke(null);
}
}
}
複製程式碼
- tld檔案和上面是一樣的,下面是測試程式碼:
<%
/*list集合*/
List list = new ArrayList();
list.add("zhongfucneng");
list.add("1");
list.add("2");
list.add("3");
request.setAttribute("list",list);
/*基本資料型別陣列*/
int[] ints = new int[]{1, 2, 3, 4, 5};
request.setAttribute("ints", ints);
/*物件陣列*/
Object[] objects = new Object[]{2, 3, 4, 5, 6};
request.setAttribute("objects", objects);
/*map集合*/
Map map = new HashMap();
map.put("aa", "aa");
map.put("bb", "bb");
map.put("cc", "cc");
request.setAttribute("map",map);
%>
List集合:
<zhongfucheng:forEach items="${list}" var="str">
${str}
</zhongfucheng:forEach>
<hr>
<hr>
基本資料型別陣列:
<zhongfucheng:forEach items="${ints}" var="i">
${i}
</zhongfucheng:forEach>
<hr>
<hr>
物件陣列:
<zhongfucheng:forEach items="${objects}" var="o">
${o}
</zhongfucheng:forEach>
<hr>
<hr>
map集合:
<zhongfucheng:forEach items="${map}" var="me">
${me.key} = ${me.value}
</zhongfucheng:forEach>
複製程式碼
- 效果:
HTML轉義標籤
要開發這個標籤就很簡單了,只要獲取到標籤體的內容,再通過經過方法轉義下標籤體內容,輸出給瀏覽器即可!
- 標籤處理器程式碼:
public class Demo1 extends SimpleTagSupport {
@Override
public void doTag() throws JspException, IOException {
//獲取到標籤體的內容再修改
StringWriter stringWriter = new StringWriter();
JspFragment jspFragment = this.getJspBody();
jspFragment.invoke(stringWriter);
String content = stringWriter.toString();
//經過filter()轉義,該方法在Tomcat可以找到
content = filter(content);
//再把轉義後的內容輸出給瀏覽器
this.getJspContext().getOut().write(content);
}
private String filter(String message) {
if (message == null)
return (null);
char content[] = new char[message.length()];
message.getChars(0, message.length(), content, 0);
StringBuffer result = new StringBuffer(content.length + 50);
for (int i = 0; i < content.length; i++) {
switch (content[i]) {
case `<`:
result.append("<");
break;
case `>`:
result.append(">");
break;
case `&`:
result.append("&");
break;
case `"`:
result.append(""");
break;
default:
result.append(content[i]);
}
}
return (result.toString());
}
}
複製程式碼
- 測試程式碼:
<zhongfucheng:filter><a href="2.jsp">你好啊</a> </zhongfucheng:filter>
<br>
<a href="2.jsp">你好啊
複製程式碼
- 效果:
if else標籤
在JSTL中並沒有if else的標籤,JSTL給予我們的是choose,when,otherwise標籤,現在我們模仿choose,when,otherwise開發標籤
思路:when標籤有個test屬性,但otherwise怎麼判斷標籤體是執行還是不執行呢?這時就需要choose標籤的支援了!choose標籤預設定義一個Boolean值為false,。當when標籤體被執行了,就把Boolean值變成true,只要Boolean值為false就執行otherwise標籤體的內容。
看程式就容易理解上面那句話了:!
- choose標籤處理器
public class Choose extends SimpleTagSupport {
private boolean flag;
@Override
public void doTag() throws JspException, IOException {
this.getJspBody().invoke(null);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
複製程式碼
- When標籤處理器
public class When extends SimpleTagSupport {
private boolean test ;
@Override
public void doTag() throws JspException, IOException {
Choose choose = (Choose) this.getParent();
//如果test為true和flag為false,那麼執行該標籤體
if (test == true && choose.isFlag() == false) {
this.getJspBody().invoke(null);
//修改父標籤的flag
choose.setFlag(true);
}
}
public void setTest(boolean test) {
this.test = test;
}
}
複製程式碼
- OtherWise標籤處理器
public class OtherWise extends SimpleTagSupport {
@Override
public void doTag() throws JspException, IOException {
Choose choose = (Choose) this.getParent();
//如果父標籤的flag為false,就執行標籤體(如果when標籤沒執行,flag值就不會被修改!when標籤沒執行,就應該執行otherwise標籤!)
if (choose.isFlag() == false) {
getJspBody().invoke(null);
//改父標籤的flag為false
choose.setFlag(true);
}
}
}
複製程式碼
- 測試程式碼:
<zhongfucheng:choose>
<zhongfucheng:when test="${user!=null}">
user為空
</zhongfucheng:when>
<zhongfucheng:otherwise>
user不為空
</zhongfucheng:otherwise>
</zhongfucheng:choose>
複製程式碼
- 效果:
DynamicAttribute介面
此介面的主要功能是用於完成動態屬性的設定!前面我們講解屬性標籤的時候,屬性都是寫多少個,用多少個的。現在如果我希望屬性可以動態的增加,只需要在標籤處理器類中實現DynamicAttribute介面即可!
現在我要開發一個動態加法的標籤:
- 標籤處理器:
public class Demo1 extends SimpleTagSupport implements DynamicAttributes {
//既然有動態屬性和動態的值,那麼我們就用一個Map集合儲存(1-1對應的關係),做的加法運算,值為Double型別的。
Map<String, Double> map = new HashMap<>();
@Override
public void doTag() throws JspException, IOException {
//定義一個sum變數用於計算總值
double sum = 0.0;
//獲取到Map集合的資料
Iterator iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Double> entry = (Map.Entry<String, Double>) iterator.next();
sum += entry.getValue();
}
//向瀏覽器輸出總和是多少
this.getJspContext().getOut().write(String.valueOf(sum));
}
//對於這個要實現的方法,我們只要關注第2個引數和第3個引數即可
//第二個參數列示的是動態屬性的名稱,第三個參數列示的是動態屬性的值
@Override
public void setDynamicAttribute(String s, String localName, Object value) throws JspException {
//將動態屬性的名字和值加到Map集合中
map.put(localName, Double.valueOf(Float.valueOf(value.toString())));
}
}
複製程式碼
- tld檔案,注意要把dynamic-attribute設定為true
<tag>
<name>dynamicAttribute</name>
<tag-class> tag.Demo1</tag-class>
<body-content> empty</body-content>
<!--這個必須要設定為true-->
<dynamic-attributes>true</dynamic-attributes>
</tag>
複製程式碼
- 測試程式碼:
<zhongfucheng:dynamicAttribute num="1.1" num2="2.2" num3="1"/>
複製程式碼
- 效果,double在運算的時候會丟失精度的,現在只是測驗下動態屬性,這裡就不詳細說了!
開發自定義函式
至於怎麼開發自定義函式,在EL表示式的部落格中有!
如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章的同學,可以關注微信公眾號:Java3y