0.说明
本博客记录的是如何显示SpringMVC框架中所有使用@RequestMapping注解标注的方法.
由于项目需要,web框架使用SpringMVC.前端\客户端\后端是分开的不同组的人,所以不可避免的要编写\更新大量的接口说明文档.这大大降低了效率,因此实现了显示SpringMVC中所有接受请求的方法的信息的功能.
总体思想:从Spring容器中找到所有加了RequestMapping注解的方法,并且集中显示.
1.用处
显示SpringMVC中所有加了@RequestMapping注解的方法信息
2.实现
2.0.环境说明
使用maven开发,别的不多说了.直接上pom
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${org.springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${org.springframework.version}</version>
<scope>test</scope>
</dependency>
其中
<org.springframework.version>3.2.13.RELEASE</org.springframework.version>
底层的spring core之类的也是3.2.13.RELEASE
2.1.显示所有请求信息
细心的同学可能发现了.在springMVC项目启动的时候,会出现很多的信息.
其中,和咱们这个文章相关的就是这个类输出的信息了:
2015-05-18 12:18:47,253 INFO org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped "{[/guide/display/search],methods=[GET],params=[],headers=[],consumes=[],produces=[application/json;charset=UTF-8],custom=[]}" onto public java.lang.String com.renren.toro.waltz.web.controller.guide.DisplayController.search()
2015-05-18 12:18:47,253 INFO org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped "{[/guide/display],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.web.servlet.ModelAndView com.renren.toro.waltz.web.controller.guide.DisplayController.index()
2015-05-18 12:18:47,253 INFO org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped "{[/guide/display/detail],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.web.servlet.ModelAndView com.renren.toro.waltz.web.controller.guide.DisplayController.detail(int)
2015-05-18 12:18:47,255 INFO org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped "{[/logout],methods=[GET],params=[],headers=[],consumes=[],produces=[application/json;charset=UTF-8],custom=[]}" onto public java.lang.String com.renren.toro.waltz.web.controller.LogoutController.logout(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
还有很多,只是截取了一段.
这个RequestMappingHandlerMapping 类会输出所有的Map信息.
下面介绍信息中的数据
这里以这一条输出作为实例:
2015-05-18 12:18:47,255 INFO org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped "{[/logout],methods=[GET],params=[],headers=[],consumes=[],produces=[application/json;charset=UTF-8],custom=[]}" onto public java.lang.String com.renren.toro.waltz.web.controller.LogoutController.logout(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
序号 | 名称 | 说明 |
1 | /logout | 对应的接受请求的url |
2 | methods | 接受请求的方法 |
3 | params | 请求的参数 |
4 | headers | 请求的header信息 |
5 | consumes | 接受请求的类型 |
6 | produces | 返回的数据类型 |
| | |
后边会输出对应的类和方法信息:
onto public java.lang.String com.renren.toro.waltz.web.controller.LogoutController.logout(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
因此从这里入手.
我这里使用了annotation-driven,所以可以直接
@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
这样就可以在一个handler方法中获取所有的添加了@RequestMapping的方法了.
具体如下:
1 // request methods
2 Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
3 Set<RequestMappingInfo> keySet = handlerMethods.keySet();
4
5 for (RequestMappingInfo requestMappingInfo : keySet) {
6 // 请求路径
7 String path = requestMappingInfo.getPatternsCondition().toString();
8
9 // 请求方法
10 String requestMethod = requestMappingInfo.getMethodsCondition().toString();
11
18 // 返回header类型
19 String responseType = requestMappingInfo.getProducesCondition().toString();
20
21 RequestMethodItem item = new RequestMethodItem();
22
23 item.setPath(path);26 item.setMethod(handlerMethod.toString().replace(" ", "<br>"));
27 item.setResponseType(responseType);
28
29 items.add(item);
30 }
这里需要写一个Item对象来存储这些信息,方便一起"打包"传给jsp页面,方便显示,由于只是一个POJO,就不帖代码了.
2.2.显示请求所对应的handler方法信息
可以使用requestMappingInfo中的getMethodsCondition()方法获取Controller类
从Map<RequestMappingInfo, HandlerMethod>这个Map中,使用对应的RequestMappingInfo作为key到这个map中查找对应的HandlerMethod
我们来具体看一下这个类:
HandlerMethod
/**
* Encapsulates information about a handler method consisting of a {@linkplain #getMethod() method}
* and a {@linkplain #getBean() bean}. Provides convenient access to method parameters,
* method return value, method annotations.
*
* <p>The class may be created with a bean instance or with a bean name (e.g. lazy-init bean,
* prototype bean). Use {@link #createWithResolvedBean()} to obtain a {@link HandlerMethod}
* instance with a bean instance resolved through the associated {@link BeanFactory}.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1
*/
一个包含了getMethod和getBean的类.
里边有用的方法:
getMethodParameters() 获得参数列表
通过这个方法,可以获得这个方法所对应的参数信息.该方法返回的是:MethodParameter[]
2.3.显示handler方法的参数信息
遍历2.2中说到的MethodParameter[] 可以获得具体的每一个参数的信息
使用其中的方法:
getParameterName(); 参数名
getParameterType(); 参数类型
getParameterAnnotations(); 参数注解
一切似乎就这样美妙的完成了.但是,学习过java class 规范的同学应该记得,javac在编译的时候,会抹去参数名称信息;
那么,getParameterName()获取到的会是什么呢?
看注释,果然如我所料:
注意returns描述的括号中的内容.
有可能是null,如果参数名称没有在class文件中,或者在开始时没有设置ParameterNameDiscoverer.
查看javac -help
输出如下:
果断加上-g参数再进行一次编译
在maven中这个参数应该加在org.apache.maven.plugins插件配置中
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArguments>
<!-- show all debug info -->
<g />
</compilerArguments>
</configuration>
</plugin>
在complierArguments中添加一个<g/>
那么,第一个问题解决了,参数名称信息已经包含在了class文件中了.
第二个问题,怎么在开始的时候指定ParameterNameDiscoverer?
查遍google stackoverflow无果...
spring doc也翻了翻,没找到,无奈,只能debug.查调用方.
思路如下,在接受参数的时候,如果不加@RequestParam注解,spring也能实现参数注入.一顿折腾.发现spring直接在spring-core中使用了asm的代码来操作字节码.最终完成了读取class文件中对应方法的的参数名
而初始化ParameterNameDiscoverer的代码在org.springframework.web.method.support.InvocableHandlerMethod类中找到了使用的方式.
直接声明了一个本地变量,使用LocalVariableTableParameterNameDiscoverer,实现类;然后调用initParameterNameDiscovery方法完成了初始化.
仔细看过LocalVariableTableParameterNameDiscoverer里边有很多虚拟机内缓存的实践,有大量的map结构用于存储数据,进而减少字节码操作.
鉴于这里只要springmvc容器启动,就不会出现class文件变动的情况,所以在本地变量中加上了static.
至此,就完成了显示所有@RequestMapping的信息的工作.
2.4.扩展功能
但是,由于只知道了对应的handlerMethod的参数,类,请求方式等信息,还是会出现可能的歧义,不够明确的指出接口的作用和意义.
所以定义了一个注解
@Inherited
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WaltzDocument {
/**
* 注释内容,推荐使用html标签进行编辑<br>
* 2015年5月7日:下午4:27:30<br>
* <br>
*
* @return
*/
String info();
/**
* 返回给页面的参数,推荐使用html标签进行编辑 <br>
* 2015年5月16日:下午4:05:32<br>
* <br>
*
* @return
*/
String[] params() default "";
/**
* 接口提供者 <br>
* 2015年5月7日:下午4:27:43<br>
* <br>
*
* @return
*/
String author();
/**
* 接口状态 <br>
* 2015年5月7日:下午4:34:06<br>
* <br>
*
* @return
* <pre>
* </pre>
*/
Status status();
public static enum Status {
/**
* 假接口
*/
fake,
/**
* 开发
*/
developing,
/**
* 开发完成
*/
done,
;
}
}
在扫描对应的handlerMethod的同时,获取对应的注解:
handlerMethod.getMethodAnnotation(WaltzDocument.class);
之后把这个注解中对应的参数一同设置到Item中.方便显示.
同时还结合了javadoc的功能,可以直接跳转到具体方法的javadoc页面查看具体说明文档.
页面上,结合一下Bootstrap和datatables就可以完成分页\筛选\搜索\排序的功能,进一步方便使用.
最终效果:
3.思考
3.1.spring中"虚拟机内"缓存的使用
在spring中,有很多的"虚拟机内换存",所谓虚拟机内缓存,利用虚拟机内的内存空间来实现一些变量的存储,当调用查询方法的时候,先使用查询的key在对应的缓存map中进行查找,这样可以大大降低底层IO操作的次数和频度.像本文中提到的LocalVariableTableParameterNameDiscoverer类,下层的获取方法参数名的方式是使用asm处理字节码.使用ClassReader和LocalVariableTableParameterNameDiscoverer提供的内部类ParameterNameDiscoveringVisitor来实现字节码操作.
数据要求不可变:这种虚拟机内缓存,对数据也是有一定的要求的.要数据的key不可变.试想,调用put方法是的hash值是A0,而想要get时hash值却变成了A1,这样就不能正确的获取数据,反而会造成缓存map越来越大,map的命中率越来越低和不断的扩容.最终导致的是大量无用内存占用和性能下降,最差情况会到导致OOM.
缓存对象大小要求有限:这种虚拟机内缓存只是用于数量有限(放置于map中的对象小于内存大小要求)的情况,试想,如果是一个无限大小的数据集合要做缓存,那最终的结构就是OOM了.否则要考虑使用一些软连接,虚连接形式的对象声明来避免这个问题了.
3.2.java参数抹去
jvm在执行方法的时候,实际上是不需要知道参数的名称的,jvm关心的只是参数的类型.所以,javac抹去了所有的参数信息,替换成paramOfString0....
测试代码如下:
public class Test {
public static void main(
String[] mainArgs) {
System.out.println("beenoisy");
}
public static void test(
int thisIsAIntArg,
String thisIsAObjectArg) {
System.out.println("Test.test()");
}
}
javac Test.java编译出来的字节码:
而同样的代码,添加了-g参数,就会带上参数名称信息,相信这是对class文件压缩的一种体现,但是却某种程度上的削弱了jvm运行时的一些功能.
同样,java对集合类型的泛型也是采取抹去处理的.在thinking in java中也说过,java的泛型不是真正的泛型.估计也是类似的考量.
3.3.怎样提高前后端开发效率
尽量减少无用功.
代码是最好的文档,然文档跟着代码改变.
一体化的文档体系\bug追踪体系\需求分派体系对于效率提升十分重要.
当然,这个小工具还是开发开始阶段紧急弄出来的,很多地方没有很好的遵循规范,后期如果有时间,考虑做成一个通用工具来集成到spring中.
最后,附上完整代码:
Controller:
1 import java.util.Arrays;
2 import java.util.Collections;
3 import java.util.List;
4 import java.util.Map;
5 import java.util.Set;
6
7 import org.springframework.beans.factory.annotation.Autowired;
8 import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
9 import org.springframework.core.MethodParameter;
10 import org.springframework.stereotype.Controller;
11 import org.springframework.web.bind.annotation.RequestMapping;
12 import org.springframework.web.method.HandlerMethod;
13 import org.springframework.web.servlet.ModelAndView;
14 import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
15 import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
16
17 import com.google.common.collect.Lists;
18 import com.renren.toro.waltz.common.annotation.WaltzDocument;
19 import com.renren.toro.waltz.common.annotation.WaltzDocument.Status;
20 import com.renren.toro.waltz.common.interceptor.performance.CountTime;
21 import com.renren.toro.waltz.dev.controller.support.RequestMethodItem;
22 import com.renren.toro.waltz.dev.controller.support.RequestMethodParameter;
23
24 /**
25 * 显示所有请求 <br>
26 * 2015年5月6日:下午5:03:51
27 *
28 * @author Keen <br>
29 */
30 @Controller
31 @RequestMapping("dev/param")
32 public class ParamController {
33 @Autowired
34 private RequestMappingHandlerMapping requestMappingHandlerMapping;
35
36 private static LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
37
38 @RequestMapping("")
39 @WaltzDocument(info = "参数显示页面", author = "ziyi.wang", status = Status.done)
40 @CountTime
41 public ModelAndView exe() {
42 // items
43 List<RequestMethodItem> items = Lists.newArrayList();
44
45 // request methods
46 Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
47 Set<RequestMappingInfo> keySet = handlerMethods.keySet();
48
49 for (RequestMappingInfo requestMappingInfo : keySet) {
50 // 请求路径
51 String path = requestMappingInfo.getPatternsCondition().toString();
52
53 // 请求方法
54 String requestMethod = requestMappingInfo.getMethodsCondition().toString();
55
56 // Controller的处理方法
57 HandlerMethod handlerMethod = handlerMethods.get(requestMappingInfo);
58
59 // 参数
60 MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
61
62 // 返回header类型
63 String responseType = requestMappingInfo.getProducesCondition().toString();
64
65 List<RequestMethodParameter> parameters = Lists.newArrayListWithExpectedSize(methodParameters.length);
66 for (MethodParameter methodParameter : methodParameters) {
67 // 参数名称
68 // 如果没有discover参数会是null.参考LocalVariableTableParameterNameDiscoverer
69 methodParameter.initParameterNameDiscovery(discoverer);
70 String parameterName = methodParameter.getParameterName();
71
72 // 参数类型
73 Class<?> parameterType = methodParameter.getParameterType();
74
75 // 参数注解
76 Object[] parameterAnnotations = methodParameter.getParameterAnnotations();
77
78 // 注解
79 String annoation = Arrays.toString(parameterAnnotations);
80
81 RequestMethodParameter parameter = new RequestMethodParameter();
82 parameter.setAnnoation(annoation);
83 parameter.setName(parameterName);
84 parameter.setType(parameterType.toString());
85 parameters.add(parameter);
86 }
87
88 WaltzDocument documentAnnotation = handlerMethod.getMethodAnnotation(WaltzDocument.class);
89
90 RequestMethodItem item = new RequestMethodItem();
91
92 item.setPath(path);
93 item.setRequestMethod(requestMethod);
94 item.setParameters(parameters);
95 item.setMethod(handlerMethod.toString().replace(" ", "<br>"));
96 item.setDocument(documentAnnotation);
97 item.setResponseType(responseType);
98
99 items.add(item);
100 }
101
102 Collections.sort(items);
103
104 ModelAndView mav = new ModelAndView("dev/param");
105 mav.addObject("items", items);
106 return mav;
107 }
108 }
POJO:
1 import java.util.List;
2
3 import com.renren.toro.waltz.common.annotation.WaltzDocument;
4
5 /**
6 * 条目 <br>
7 * 2015年5月7日:下午3:26:15
8 *
9 * @author Keen <br>
10 */
11 public class RequestMethodItem implements Comparable<RequestMethodItem> {
12 private String path;
13 private String requestMethod;
14 private String method;
15 private String responseType;
16 private List<RequestMethodParameter> parameters;
17 private WaltzDocument document;
18
19 public String getPath() {
20 return path.replace("[", "").replace("]", "");
21 }
22
23 public void setPath(
24 String path) {
25 this.path = path;
26 }
27
28 public String getRequestMethod() {
29 return requestMethod;
30 }
31
32 public void setRequestMethod(
33 String requestMethod) {
34 this.requestMethod = requestMethod;
35 }
36
37 public String getMethod() {
38 String a = getA();
39 return method + a;
40 }
41
42 private String getA() {
43 String methodName = method.split("<br>")[2];
44
45 String classPath = methodName.split("\\(")[0];
46
47 String[] packageSplit = classPath.split("\\.");
48 StringBuilder sb = new StringBuilder();
49 for (int i = 0; i < packageSplit.length; i++) {
50 sb.append(packageSplit[i]);
51 if (i < packageSplit.length - 1) {
52 if (i == packageSplit.length - 2) {
53 sb.append(".html#");
54 } else {
55 sb.append("/");
56 }
57 }
58 }
59
60 sb.append("(");
61 String methodPath = methodName.split("\\(")[1];
62 sb.append(methodPath.replace(",", ", "));
63
64 String href = sb.toString();
65 String a = "<br>\n<a href='http://svn.d.xiaonei.com/toro/waltz/renren-toro-waltz-web/trunk/doc/" + href
66 + "' target='_blank'>详细接口文档</a>";
67 return a;
68 }
69
70 public void setMethod(
71 String method) {
72 this.method = method;
73 }
74
75 public List<RequestMethodParameter> getParameters() {
76 return parameters;
77 }
78
79 public void setParameters(
80 List<RequestMethodParameter> parameters) {
81 this.parameters = parameters;
82 }
83
84 public WaltzDocument getDocument() {
85 return document;
86 }
87
88 public void setDocument(
89 WaltzDocument document) {
90 this.document = document;
91 }
92
93 public String getResponseType() {
94 return responseType;
95 }
96
97 public void setResponseType(
98 String responseType) {
99 this.responseType = responseType;
100 }
101
102 public String getResponseParams() {
103 WaltzDocument document = this.getDocument();
104 if (document == null) {
105 return "";
106 }
107 String[] params = document.params();
108 StringBuilder sb = new StringBuilder();
109 for (int i = 0; i < params.length; i++) {
110 sb.append(params[i]).append("<hr>\n");
111 }
112
113 return sb.toString();
114 }
115
116 @Override
117 public String toString() {
118 return "Item [path=" + path + ", requestMethod=" + requestMethod + ", method=" + method + ", responseType="
119 + responseType + ", parameters=" + parameters + ", document=" + document + "]";
120 }
121
122 @Override
123 public int compareTo(
124 RequestMethodItem o) {
125 return this.getPath().compareTo(o.getPath());
126 }
127 }
1 /**
2 * 参数 <br>
3 * 2015年5月7日:下午3:26:31
4 *
5 * @author Keen <br>
6 */
7 public class RequestMethodParameter {
8 private String name;
9 private String annoation;
10 private String type;
11
12 public String getName() {
13 return name;
14 }
15
16 public void setName(
17 String name) {
18 this.name = name;
19 }
20
21 public String getAnnoation() {
22 return annoation;
23 }
24
25 public void setAnnoation(
26 String annoation) {
27 this.annoation = annoation;
28 }
29
30 public String getType() {
31 return type;
32 }
33
34 public void setType(
35 String type) {
36 this.type = type;
37 }
38
39 @Override
40 public String toString() {
41 return "Parameter [name=" + name + ", annoation=" + annoation + ", type=" + type + "]";
42 }
43 }
jsp
1 <%@ page language="java" pageEncoding="UTF-8"%>
2 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
3 <%@ page import="java.util.Arrays"%>
4 <html>
5 <head>
6 <title>Waltz 接口查看页</title>
7
8 <!-- 新 Bootstrap 核心 CSS 文件 -->
9 <link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.4/css/bootstrap.min.css">
10
11 <!-- 可选的Bootstrap主题文件(一般不用引入) -->
12 <link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.4/css/bootstrap-theme.min.css">
13
14 <!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
15 <script src="http://cdn.bootcss.com/jquery/1.11.2/jquery.min.js"></script>
16
17 <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
18 <script src="http://cdn.bootcss.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
19
20
21 <!-- datatables -->
22 <script type="text/javascript" charset="utf8" src="http://cdn.datatables.net/1.10.7/js/jquery.dataTables.min.js"></script>
23 <script type="text/javascript" charset="utf8" src="http://cdn.datatables.net/plug-ins/1.10.7/integration/bootstrap/3/dataTables.bootstrap.js"></script>
24
25
26 </head>
27 <body>
28 <table id="paramTable" class="table table-striped table-hover table-bordered table-condensed display">
29 <thead>
30 <tr>
31 <th>No.</th>
32 <th>docment</th>
33 <th>author</th>
34 <th>status</th>
35 <th>path</th>
36 <th>requestMethod</th>
37 <th>responseType</th>
38 <th>responseParams</th>
39 <th>controllerMethod</th>
40 <th>parameters</th>
41 </tr>
42
43 </thead>
44 <tbody>
45 <c:forEach items="${items}" var="i" varStatus="x">
46 <tr>
47 <td>${x.count}</td>
48 <td>${i.document.info()}</td>
49 <td>${i.document.author()}</td>
50 <td>${i.document.status()}</td>
51 <td>${i.path}<br><a href='http://xxx.com${i.path}' target='_blank'>跳转查看</a></td>
52 <td>${i.requestMethod}</td>
53 <td>${i.responseType}</td>
54 <td>${i.responseParams}</td>
55 <td>${i.method}</td>
56 <td>
57 <c:if test="${i.parameters.size() > 0 }">
58 <table class="table table-striped table-hover table-bordered table-condensed">
59 <thead>
60 <tr>
61 <th>parameterName</th>
62 <th>parameterType</th>
63 <th>parameterAnnotations</th>
64 </tr>
65 </thead>
66 <tbody>
67 <c:forEach items="${i.parameters}" var="p">
68 <tr>
69 <td>${p.name}</td>
70 <td>${p.type}</td>
71 <td>${p.annoation}</td>
72 </tr>
73 </c:forEach>
74 </tbody>
75 </table>
76 </c:if>
77 </td>
78 </tr>
79 </c:forEach>
80 </tbody>
81 <tfoot>
82 <tr>
83 <th>No.</th>
84 <th>docment</th>
85 <th>author</th>
86 <th>status</th>
87 <th>path</th>
88 <th>requestMethod</th>
89 <th>responseType</th>
90 <th>responseParams</th>
91 <th>controllerMethod</th>
92 <th>parameters</th>
93 </tr>
94 </tfoot>
95 </table>
96
97 <script type="text/javascript">
98 $(document).ready(function() {
99 // Setup - add a text input to each footer cell
100 $('#paramTable tfoot th').each( function () {
101 var title = $('#paramTable thead th').eq( $(this).index() ).text();
102 $(this).html( '<input type="text" placeholder="Search '+title+'" />' );
103 });
104
105 // DataTable
106 var table = $('#paramTable').DataTable({
107 paging: false
108 });
109
110 // Apply the search
111 table.columns().eq(0).each(function(colIdx){
112 $('input', table.column(colIdx).footer()).on('keyup change',function () {
113 table.column( colIdx ).search( this.value ).draw();
114 });
115 });
116 });
117 </script>
118
119 </body>
120 </html>