在传统的java web开发过程中,我们使用url通知后台服务器调用java代码。
    不管是页面submit还是ajax数据提交,总是要提供一个url。结果我们看到,jsp或者js中到处充斥着url代码(就像当年jsp中到处充斥着java代码一样)。
    有时,服务器的某功能的url地址发生改变了(比如我以前参与的一个struts1的项目,struts-config-module改变了,或者删除了),那么要到页面中找到所有写了这个url地址的页面,一个一个给修改来。如果项目涉及5000过个jsp,而有的jsp中的路径中的一部分是由变量生成出来的,那么找到这些url并修正简直是一场噩梦。当然,可以在整个工程中进行批量替换,但是由此引起的系统变化,总会给担心质量的人造成很大的心理负担。
    最早的时候,我看到页面上有java代码让我感觉心理很没底,现在这个ajax横行的年代,很难让一个程序员有自制力去管理好他的url,页面上乱七八糟的url带着匪夷所思的问号及参数,更让人觉得无所适从。调试和系统维护的时候,从一个url找到这个url最终调用的java类及方法,还是要费一番脑筋和周折的。
    感谢dwr的出现,事情好像有了一些转机。我有幸在一个项目中用到了dwr技术,那是三年前,dwr的版本应该比较低,需要在一个配置文件中配置dwr调用需要的java类、方法好像还有参数或者返回值的类型。然后,在页面中,还要引入两个dwr的js,一个好像是dwr-core,另外一个是和刚刚在配置文件中相关的java类的名字相同的一个js,这个js好像是动态生成的。当时我刚学习java技术,在一家公司中从事程序员,都是把别人的代码复制过来改改,没有做深入的研究。当时虽然没有明白dwr的原理,但是还是感谢dwr可以让我偷懒:我不用再配置struts-congfig-xyz.xml,不用在action里加if,else,不用在页面上写讨厌的url。只要声明一个dwr配置文件,一切都好办了。
    后来,就是最近几天,我在搭建一个最小的开发框架(也就是组装网上一些开源的框架)时,意识到我又将面对那些url:项目组其它人在使用这个小开发框架开发系统时,又会在jsp上把url写的到处都是。于是我想象了一种网页请求服务器的开发方式,在jsp页面中,如果能写如下代码,就好了: import com.siqisoft.dushuhui.book.controller.RecommendBookAction;
 
 RecommendBookAction recommendBookAction = new RecommendBookAction();
 bookName = "<三国演义>";
 recommendBookAction.addBook(bookName);    -嗯?又在jsp页面中写java代码?
    -不是的,我是希望能用javascript写出以上的代码,用javascript调用java类。
    -开玩笑,是不是不懂?javascript是jsp被编译输入为html到了浏览器端才执行的代码,写成上面那个样子,怎么可能调用java啊!你在页面<scirpt>标签中写上这个,真让人笑掉大牙啊!再说javascript的语法都不支持import!    好吧,javascript既然不支持这种写法,那么我退一步,我希望我的jsp将来是这样的:   <%@ include file="/taglib.jsp"%>
   <base:import name="com.siqisoft.dushuhui.book.controller.RecommendBookAction" />
   <script>
       recommendBookAction.addBook(bookName);
   </script>    看起来好像有点儿晕.
    那么我来解释一下: javascript不是不支持import,那么我想在页面上应该有一个和java类同名的javascript类,这个javascript类中的方法和java中的方法是一样一样的。
    然后,只要调用这个javascript的类的方法,这个javascript类的方法就去找到它对应的java类在web服务器上的url,并请求。我的同事们只管像java一样使用javascript调用java方法就可以了,不用再去处理恼人的url了。javascript类的方法寻找服务器url的事情,不需要程序员去对应,只要交给base:import这个jsp标签去做就可以了。
    所以,上面代码的第一行的作用是引入一个标签库,第二行使用import标签,引入了一个java类。那么这个标签到底做了什么呢?
    1、遍历name参数的java类中所有方法,算出这些方法在web服务器上的url(如果这个方法真的在web服务器上有url的话)。
    2、根据这些有url的java类的方法,生成一个javascript类,输出到页面。这javascript类的放和java中的方法名一样。
方法体内就是使用ajax请求远程服务器。    其中第一步算url的时候,不同的框架有不同的算法,如SpringMVC,可以从一些缓存中获得到一些url和java方法的映射关系。第二步javascript的ajax调用也可以选用不同的框架。    下面介绍一下我的做法:
    

解决方案 »

  1.   

    1、获得java方法和url的映射关系:
       我使用的是springMVC框架,同样因为不喜欢url的原因,我在controller类中使用@ResultMapping注解的时候,从来不加参数(我猜这样做可能不安全),但是如果这样的话,org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping无法帮我映射controller的方法到url。于是我复制了DefaultAnnotationHandlerMapping的源代码,改了改形成了自己映射风格:   package com.siqisoft.core.framework.spring.framework;import java.lang.reflect.Method;
    import java.util.LinkedHashSet;
    import java.util.Set;import org.springframework.context.ApplicationContext;
    import org.springframework.core.annotation.AnnotationUtils;
    import org.springframework.stereotype.Controller;
    import org.springframework.util.ReflectionUtils;
    import org.springframework.util.StringUtils;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMapping;import com.siqisoft.core.framework.spring.framework.events.ClassAndUrlMappedEvent;public class AnnotationHandlerMapping extends AbstractDetectingUrlHandlerMapping { private String subKeyWord = ""; private String suffix = ""; public void setSubKeyWord(String subKeyWord) {
    this.subKeyWord = subKeyWord;
    } public void setSuffix(String suffix) {
    this.suffix = suffix;
    } @Override
    protected String[] determineUrlsForHandler(String beanName) {
    ApplicationContext context = getApplicationContext();
    Class<?> handlerType = context.getType(beanName);
    if (AnnotationUtils.findAnnotation(handlerType, Controller.class) != null) {
    return determineUrlsForHandlerMethods(handlerType);
    } else {
    return null;
    }
    } /**
     * 根据传入的类,遍历这个类的所有方法. 如果方法上标记了RequestMapping注释
     * 并且RequestMapping没有value参数(这个并且是为了兼容springMvc的默认模式)那么采取这样的映射方式:
     * 类的全路径名先将'.'换为'/',然后去掉成员变量subKeyWord中包含的以';'分隔的关键字,最后+方法名.do
     * 
     * @param handlerType
     * @return
     */
    protected String[] determineUrlsForHandlerMethods(Class<?> handlerType) {
    final ApplicationContext objApplicationContext = this.getApplicationContext();
    final Set<String> urls = new LinkedHashSet<String>();
    ReflectionUtils.doWithMethods(handlerType, new ReflectionUtils.MethodCallback() {
    public void doWith(Method method) {
    RequestMapping mapping = AnnotationUtils.findAnnotation(method, RequestMapping.class);
    if (mapping != null) {
    String[] mappedPatterns = mapping.value();
    if (mappedPatterns.length == 0) {
    String className = method.getDeclaringClass().getName();
    if (className.indexOf('$') == -1) {// 过滤掉cglib自动生成的类型
    // 处理url
    String urlPath = className;
    urlPath = urlPath.replace('.', '/');
    String[] subKeyWords = subKeyWord.split(";");
    for (int i = 0; i < subKeyWords.length; i++) {
    urlPath = urlPath.replaceFirst(subKeyWords[i], "");
    }
    String methdName = method.getName();
    urlPath += "/" + methdName + suffix;
    urls.add(urlPath);
    // 发布事件
    ClassAndUrlMappedEvent classAndUrlMappedEvent = new ClassAndUrlMappedEvent(className);
    classAndUrlMappedEvent.setMothodName(methdName);
    classAndUrlMappedEvent.setUrl(urlPath);
    objApplicationContext.publishEvent(classAndUrlMappedEvent);
    }
    }
    }
    }
    }, ReflectionUtils.USER_DECLARED_METHODS);
    // 准备事件
    return StringUtils.toStringArray(urls);
    }
    }    我每次发现一个类的方法到url的映射的时候,我都发一个事件告诉我的一个缓存,这个缓冲同学就根据这个时间,准备javascript类。下面是缓冲器的代码:(写的很纠结,因为我在url映射器中发出的事件有些不伦不类。)
    package com.siqisoft.core.framework.jswork;import java.util.HashMap;
    import java.util.Map;import org.apache.log4j.Logger;
    import org.springframework.context.ApplicationListener;
    import org.springframework.stereotype.Component;import com.siqisoft.core.framework.spring.framework.events.ClassAndUrlMappedEvent;@Component
    public class ImportGenerate implements ApplicationListener<ClassAndUrlMappedEvent> { private static Map<String, String> cachedJavaScriptClass = new HashMap<String, String>(); private static String VAR_NAME = "${varName}"; protected Logger logger = Logger.getLogger(this.getClass()); public static String getJavaScritptClass(String className, String varName) {
    String strJavaScript = cachedJavaScriptClass.get(className);
    if (strJavaScript != null) {
    strJavaScript = strJavaScript.replace(VAR_NAME, varName);
    } else {
    strJavaScript = " <!-- 找不到类:" + className + " 对应的URL,所以无法为其生成javascript类 -->";
    }
    return strJavaScript == null ? "" : strJavaScript;
    } public void onApplicationEvent(ClassAndUrlMappedEvent event) {
    String className = event.getClassName();
    String method = event.getMothodName();
    String url = event.getUrl();
    compileClass(className, method, url);
    } /**
     * 编译javascript类中的方法
     * 
     * @param methodName
     * @param url
     * @return
     */
    private String compileMethod(String methodName, String url) {
    String result = " "+methodName + ":function(callBack,extraData,formId){\r\n";
    result += " callRemoteJava('" + url + "',callBack,extraData,formId,onFailed);\r\n";
    result += " }\r\n";
    return result;
    } /**
     * 编译javascript类
     * 
     * @param className
     * @param varName
     * @return
     */
    private void compileClass(String className, String methodName, String url) {
    String strCode = cachedJavaScriptClass.get(className);
    if (strCode != null) {
    strCode = strCode.replace(" };\r\n", " ,\r\n");
    strCode = strCode.replace(" </script>\r\n", "");
    } else {
    strCode = "<script type=\"text/javascript\" language=\"javascript\">\r\n";
    strCode += " //类" + className + "对应的javascript类\r\n";
    strCode += " var " + VAR_NAME + " = {\r\n";
    } strCode += " " + compileMethod(methodName, url); strCode += " };\r\n";
    strCode += " </script>\r\n";
    cachedJavaScriptClass.put(className, strCode);
    }}
    好了,我现在已经把所有类的方法和url的映射关系存储到ImportGenerate类的cachedJavaScriptClass成员变量中了。我相信springMVC铁定是有缓存的,但是我没有使用它的缓存。原因很简单:因为我找不到它缓存在什么地方。下面是接轨的impot标签:
    package com.siqisoft.core.framework.jswork;import java.io.IOException;import javax.servlet.jsp.JspTagException;
    import javax.servlet.jsp.JspWriter;
    import javax.servlet.jsp.tagext.Tag;
    import javax.servlet.jsp.tagext.TagSupport;public class ImportTag extends TagSupport { private String name; private String var; public int doStartTag() throws JspTagException {
    JspWriter out = pageContext.getOut();
    try {
    if (var == null || "".equals(var.trim())) {
    var = name.substring(name.lastIndexOf('.') + 1);
    }
    out.print(ImportGenerate.getJavaScritptClass(name, var));
    } catch (IOException ex) {
    throw new JspTagException("IOError:" + ex.getMessage());
    }
    return Tag.SKIP_BODY;
    } public int doEndTag() throws JspTagException {
    this.name="";
    this.var="";
    return Tag.EVAL_PAGE;
    } public String getName() {
    return name;
    } public void setName(String name) {
    this.name = name;
    } public String getVar() {
    return var;
    } public void setVar(String var) {
    this.var = var;
    }}为了防止同名不同路径的类在同一个jsp中被引入的极端情况出现,import标签支持var属性,自定义javascript类的名字。第一步就说完了,如果有如下类:package com.siqisoft.dushuhui.book.controller;import javax.servlet.http.HttpServletRequest;import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.servlet.ModelAndView;import com.siqisoft.dushuhui.book.model.Book;
    import com.siqisoft.dushuhui.book.model.ReadHistory;
    import com.siqisoft.dushuhui.book.model.Section;
    import com.siqisoft.dushuhui.book.service.BookService;
    import com.siqisoft.dushuhui.book.service.SectionService;@Controller
    public class ReadBookAction { @Autowired
    private BookService bookService; @Autowired
    private SectionService sectionService; public void setBookService(BookService bookService) {
    this.bookService = bookService;
    } @RequestMapping
    public ModelAndView readBook(ReadHistory readHistory, HttpServletRequest request) throws Exception {
    int iBookId = readHistory.getBookId();
    Book book = bookService.readBook(iBookId);
    ModelAndView mv = new ModelAndView();
    mv.addObject(book);
    mv.addObject(readHistory);
    return mv;
    } @RequestMapping
    public ModelAndView readNextSection(Section section, HttpServletRequest request) throws Exception {
    sectionService.loadSection(section);
    ModelAndView mv = new ModelAndView();
    mv.addObject(section);
    return mv;
    }}
    那么在jsp页面上可以这样写:<base:import name="com.siqisoft.dushuhui.book.controller.ReadBookAction" />最后生成的HTML代码是这样的: <script type="text/javascript" language="javascript">
    //类com.siqisoft.dushuhui.book.controller.ReadBookAction对应的javascript类
    var ReadBookAction = {
    readBook:function(callBack,extraData,formId){
    callRemoteJava('/dushuhui/book/ReadBookAction/readBook.do',callBack,extraData,formId,onFailed);
    }
    ,
    readNextSection:function(callBack,extraData,formId){
    callRemoteJava('/dushuhui/book/ReadBookAction/readNextSection.do',callBack,extraData,formId,onFailed);
    }
    };
    </script>至于 readBook、readNextSection中调用的是什么飞机,我们下帖再说!
      

  2.   

    说说第二步:javascript的ajax调用,这里使用的是mootools。
    【为什么这么非主流,为什么不是struts2+jquery?这和有的人喜欢吃榴莲有的人不喜欢吃是一样的。从技术选型角度讲,列举一大推这个的好处,那个的坏处,都是纸上谈兵。所以,总结的出来三个字:我喜欢。】页面javascript调用java,就好像COM技术一样:接口协议+数据类型。上贴我们说了接口协议,就是一个简单的封装,下面来说说数据类型,更具体的说:数据怎么在java和javascript间传递:
        1、传参:springMVC的controller中的方法,第一个参数就是model,这个model是springMVC在request中自己拼出的,所以,javascript给java传参,只要把当前页面的form提交给后台就可以了。
        2、返回值:那么java给javascript返回值,怎么办?只有回调函数一种办法。不如在调用的时候指定:
        
           ReadBookAction.readBook(function(result){
               alert(result);
           });
        
        这个result就是java的返回值。从宏观上看,无论是java返回了json、xml、html,都算java的返回值,在这里都能接收到,建议使用json,不使用html,这个完全拆开前后台,说不定哪天可以用现有的服务器代码,前端给换成了EclipseRCP呢(意淫一下)。    参考上贴生成的HTML中的js代码,简单说一下我的处理方式:
        如果有form中不包含的参数,放到extraData中,如果页面有多个form,使用formId指定参数,如果服务出错了,要自定义错误处理,那么传进去onFailed进行处理。    下面贴一下使用mootools实现的callRemoteJava方法:
         
    //用javascript调用java的方法
    function callRemoteJava(url,onSuccess,extraData,formId,onFailed){
    //获得到Form
    var form =  getForm(formId);
    //获得到额外的数据
    var tempFields = null;
    if(extraData!=undefined){
    tempFields = setDataToForm(form,extraData);
    }
    //注册回调事件
    var request = form.get('send');
    //成功
    if(onSuccess!=undefined&&onSuccess!=null){
    request.addEvent('success',onSuccess);
    }else{
    request.addEvent('success',function(){});
    }
    //失败
    if(onFailed!=undefined&&onFailed!=null){
    request.addEvent('failure',onFailed);
    }else{
    request.addEvent('failure',callJavaFailed);
    }

    //向后台请求数据
    form.send(url);
    //清除掉临时域
    if(tempFields != null){
    for(var i =0 ; i< tempFields.length;i++){
    tempFields[i].destroy();
    }
    }
    }//获得表单对象
    function getForm(formId){
    var form = $(formId);
    if(form==null){
    //寻找document内的第一个Form元素,如果找不到到,就自己构造一个
    var forms = $$("form");
    if(forms.length>0){
    form = forms[0];
    }else{
    form = new Element("form");
    form.set('name','tempForm');
    form.inject($(document.body),'top');
    }
    }
    return form;
        
    }//获得数据对象
    function setDataToForm(form,extraData){
    //判断Form中是否有data中的值,没有的话,增加到Form.有的话,meger掉Form中的值
    var tempFields = new Array();
    if("array"!=typeOf(extraData)){
    alert("调用java类时,非Form内的额外参数必须使用Array对象传递,否则这些数据将不会被处理");
    }else{
    //遍历额外的数据
    for(prop in extraData){
    if("string"==typeOf(extraData[prop])){
    var field = form.getElement('[name='+prop+']');
    //如果Form中不包含此域,创建一个临时的Field域
      if(field==null){
      field = new Element('input',{'type':'hidden','name':prop});
      field.inject(form,'top');
      tempFields.push(field);
      }
      //如果Form中包含此域,用extraData的值覆盖Field的值
      field.set('value',extraData[prop]);
    }
    }
    }
    return tempFields;
    }//请求出错是调用的函数
    function callJavaFailed(instance){
    alert('操作失败!');
    }
           快吃饭了,此贴完!
      
      如有意交流、鄙视可以回帖。
      

  3.   


    对ajax两端的入口进行了简单的封装。
      

  4.   

    楼主 我说DWR已经做了 不是为了打击你。你有思路 能做出来 实力还是很强的。只是你做出来后,回头参考下DWR的做法(如果有源码) 再对比下自己的 相信对你有所帮助 共勉!
      

  5.   

    谢谢,谢谢!你提的建议非常好。     不过说老实话,我看不懂网上那些开源的源代码,它们设计的太通用,往往是接口调用接口,考虑的太抽象,而我只想使用它解决的其中的某一个问题的功能。我曾经下载了springMVC的源代码贴在eclipse里面调试,接口调接口,调的我都头晕了。    以前吃了好多亏,才意识到技术上,通用不是目的,最重要的是可控。因为我能力实在有限,想了一下,还是自己实现一个够用的自行车轮,而不是引入高铁的车轮,以免雷雨天追尾。
      

  6.   

    很桑心,这个贴不如那个说java是垃圾的贴火。