操作手册:DWR使用原理

总文档 :文章目录
Github : github.com/black-ant

一 . 前言

DWR 是一个可以用于前后端交互的工具 , 其本质是一个长连接 , 它具有以下的主要功能 :

  • 前端JS直接调用后端方法
  • 后端代码直接运行前端JS

DWR 这个工具是一个比较有历史的工具了 , 在集群或者其他特殊环境中 , 具有很大的局限性 , 相对而言其实有很多更合适的解决方案 , 这一篇主要是对其设计的思路进行一个分析 .

来看看早期的前后端强耦合的意义和设计思路.

二 . 使用

使用主要分为以下几个步骤 :

  • 配置准备
  • 准备 Java 端 service
  • 准备 DwrScriptSessionManager 用于Java 端调用 Web 端
  • 前端使用

2.1 配置准备

准备配置Bean

配置的 Bean 准备没有太多东西 , 这个Bean 主要用于生成一个 ServletRegistrationBean , 并且为其配置了多个属性

ServletRegistrationBean : ServletContextInitializer是用于在Servlet 3.0容器中注册 Servlet

    /**
     * @param springDwrServlet SpringDwrServlet
     * @return ServletRegistrationBean
     */
    @Bean
    public ServletRegistrationBean registDwrServlet(SpringDwrServlet springDwrServlet) {
        ServletRegistrationBean servletRegister = new ServletRegistrationBean(springDwrServlet, "/dwr/*");
        Map<String, String> initParameters = new HashMap<String, String>();
        initParameters.put("debug", "true");
        initParameters.put("activeReverseAjaxEnabled", "true");
        initParameters.put("pollAndCometEnabled", "true");
        servletRegister.setInitParameters(initParameters);
        return servletRegister;
    }
   

复制代码

准备配置 XML : dwr-spring-config.xml

主要是开启注解 , 设置扫描路径

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:dwr="http://www.directwebremoting.org/schema/spring-dwr"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

       http://www.directwebremoting.org/schema/spring-dwr http://www.directwebremoting.org/schema/spring-dwr-3.0.xsd">

    <dwr:annotation-config/>
    <dwr:configuration/>
    <dwr:annotation-scan base-package="com.gang.comgangcasedwr" scanDataTransferObject="true" scanRemoteProxy="true"/>
</beans>

复制代码

注意要导入该配置 : @ImportResource(locations = "classpath:dwr-spring-config.xml")

2.2 准备 Java 端

  • @RemoteProxy : 远程代理类
  • @RemoteMethod : 远程方法
@Service
@RemoteProxy
public class Demo2Service {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * @return
     */
    @RemoteMethod
    public String hello() {
        return "hello";
    }

    /**
     * @param msg
     * @return
     */
    @RemoteMethod
    public String echo(String msg) {
        return msg;
    }

}

复制代码

2.3 其他类

DwrContainer : 用于扩展容器

  • 从Spring上下文中指定的配置中查找所有bean
  • 从Spring web应用程序上下文中加载配置
public class SpringDwrContainer extends SpringContainer {

    /**
     * @see org.directwebremoting.spring.SpringContainer#addParameter(java.lang.String, java.lang.Object)
     */
    @Override
    public void addParameter(String askFor, Object valueParam) throws ContainerConfigurationException {

        try {
            Class<?> clz = ClassUtils.forName(askFor, ClassUtils.getDefaultClassLoader());

            @SuppressWarnings("unchecked")
            Map<String, Object> beansOfType = (Map<String, Object>) ((ListableBeanFactory) beanFactory)
                    .getBeansOfType(clz);

            if (beansOfType.isEmpty()) {
                super.addParameter(askFor, valueParam);
            } else if (beansOfType.size() > 1) {
                String key = StringUtils.uncapitalize(SpringDwrServlet.class.getSimpleName());
                if (beansOfType.containsKey(key)) {
                    beans.put(askFor, beansOfType.get(key));
                } else {
                    throw new ContainerConfigurationException("spring容器中无法找到对应servlet:" + key);
                }
            } else {
                beans.put(askFor, beansOfType.values().iterator().next());
            }
        } catch (ClassNotFoundException ex) {
            super.addParameter(askFor, valueParam);
        }

    }
}


复制代码

SpringDwrServlet : 构建 DWRServlet

DWRServlet 的主要实现 : 处理所有对DWR的调用的servlet

DWRServlet从Spring IoC容器中检索配置。这可以通过两种方式实现 :

  • 使用Spring命名空间。当为DWR使用Spring名称空间时,这个servlet会自动获取DWR的配置
  • 显式地指定要选取的配置。
@Component
public class SpringDwrServlet extends DwrSpringServlet {

    /**  */
    private static final long serialVersionUID = 1L;

    @Override
    protected SpringContainer createContainer(ServletConfig servletConfig) {
        ApplicationContext appContext = getApplicationContext(servletConfig.getServletContext());

        SpringDwrContainer springContainer = new SpringDwrContainer();
        springContainer.setBeanFactory(appContext);
        StartupUtil.setupDefaultContainer(springContainer, servletConfig);
        return springContainer;
    }
}

复制代码

DwrScriptSessionManagerUtil : 添加 container

ScriptSessionListener : 该对象是一个监听器 , 当web应用程序中的活动会话列表发生更改时,将通知此接口的实现 , 即监听变化时处理

public class DwrScriptSessionManagerUtil extends DwrServlet {

    private Logger logger = LoggerFactory.getLogger(this.getClass());


    /**
     *
     * @throws ServletException
     */
    public void init() throws ServletException {

        Container container = ServerContextFactory.get().getContainer();
        ScriptSessionManager manager = container.getBean(ScriptSessionManager.class);
        ScriptSessionListener listener = new ScriptSessionListener() {

            public void sessionCreated(ScriptSessionEvent ev) {
                HttpSession session = WebContextFactory.get().getSession();
                String userId = (String) session.getAttribute("userId");
                System.out.println("a ScriptSession is created!");
                ev.getSession().setAttribute("userId", userId);
            }

            public void sessionDestroyed(ScriptSessionEvent ev) {
                System.out.println("a ScriptSession is distroyed");
            }
        };
        manager.addScriptSessionListener(listener);
    }

    public void init(final String key, final String value) throws ServletException {
        Container container = ServerContextFactory.get().getContainer();
        ScriptSessionManager manager = container.getBean(ScriptSessionManager.class);
        ScriptSessionListener listener = new ScriptSessionListener() {
            public void sessionCreated(ScriptSessionEvent ev) {
//                HttpSession session = WebContextFactory.get().getSession();
                //String userId = ((User) session.getAttribute("userinfo")).getHumanid() + "";
                logger.info("a ScriptSession is created!");
                ev.getSession().setAttribute(key, value);
            }

            public void sessionDestroyed(ScriptSessionEvent ev) {
                logger.info("a ScriptSession is distroyed");
            }
        };
        manager.addScriptSessionListener(listener);

    }
}

复制代码

2.4 前端源码

其中主要是多个工具 js , 直接获取即可 :

<html>
<head>
    <title></title>
    <script type='text/javascript' src='/dwr/engine.js'></script>
    <script type='text/javascript' src='/dwr/util.js'></script>
    <script type='text/javascript' src='/dwr/interface/Demo2Service.js'></script>
    <script type='text/javascript' src='/dwr/interface/Demo1Service.js'></script>
</head>

<div>hello</div>
<input type="button" onclick="onpage()" value="接收后端推送消息">
<script>
    //  激活ajax
    dwr.engine.setActiveReverseAjax(true)
    // 页面未加载的时候是否发送通知
    dwr.engine.setNotifyServerOnPageUnload(true, true)
    // 出现错误后的处理方法
    dwr.engine.setErrorHandler(function () {
        console.log("Handler error")
    })

    Demo2Service.echo('回声测试', function (str) {
        alert(str);
    });

    function onpage() {
        // 页面加载直接调用这个函数,我这块使用点击按钮
        Demo1Service.onPageLoad("后端功能开启");
    }

    // 后端会调用这个函数
    function getmessage(data) {
        console.log("data =" + data)
        alert(data);
    }


</script>
</html>


复制代码

三 .简单看看前端源码

DWR 核心是建立一个长连接 , 通过其本身封装的 JS 和 Servlet ,通过 Invoke 代理类实现相互调用

DWR001.jpg

以下是 DWR 的前端结构

DWR002.jpg

先简单过一下前端 JS 的调用 :

可以看到每个 @RemoteProxy 标注的类都有一个 对应的 JS :

if (typeof dwr == 'undefined' || dwr.engine == undefined) throw new Error('You must include DWR engine before including this file');

(function() {
  if (dwr.engine._getObject("Demo2Service") == undefined) {
    var p;
    
    p = {};

    // 对应上文 本地的 echo
    p.echo = function(p0, callback) {
      return dwr.engine._execute(p._path, 'Demo2Service', 'echo', arguments);
    };

     // 对应上文 本地的 hello 方法
    p.hello = function(callback) {
      return dwr.engine._execute(p._path, 'Demo2Service', 'hello', arguments);
    };
    // 对应远程类
    dwr.engine._setObject("Demo2Service", p);
  }
})();

复制代码

核心都是通过一个 JS 对象 dwr.engine 来完成 , 这个 execute 做了这些事


dwr.engine._execute = function(overridePath, scriptName, methodName, args) {
    var path = overridePath || dwr.engine._effectivePath();
    dwr.engine._singleShot = false;
    if (dwr.engine._batch == null) {
      dwr.engine.beginBatch();
      dwr.engine._singleShot = true;
    }

    var batch = dwr.engine._batch;

    // All the paths MUST be to the same servlet
    if (batch.path == null) {
      batch.path = path;
    }
    else {
      if (batch.path != path) {
        dwr.engine._handleError(batch, { name:"dwr.engine.multipleServlets", message:"Can't batch requests to multiple DWR Servlets." });
        return;
      }
    }

    // 主要调用
    dwr.engine.batch.addCall(batch, scriptName, methodName, args);

    if (dwr.engine._isHeartbeatBatch(batch)) {
      // Heartbeats should fail fast.
      batch.timeout = 750;
    }

    // Now we have finished remembering the call, we increment the call count
    batch.map.callCount++;
    if (dwr.engine._singleShot) {
      return dwr.engine.endBatch();
    }
  };

复制代码

image-20210409105226969.png

DWR003.jpg

四 . 后端源码分析

4.1 一切的起点

核心还是通过一个 HttpServlet 类实现的 :


public class DwrServlet extends HttpServlet

// 其中还有耳熟能详的 
public void doGet(HttpServletRequest req, HttpServletResponse resp) 
public void doPost(HttpServletRequest request, HttpServletResponse response)
 
// 后文我们能看到 , DWR Servlet 是怎么处理这个方法的
 

复制代码

4.2 核心逻辑

核心逻辑主要是以下几个步骤 :

Step 1 : Servlet 对请求做出拦截
Step 2 : 获取请求的属性 (UrlProcessor) , 获取一个 CreatorModule
Step 3 : 通过该 Module 获取其中的 creator (Module 类似于一个模块体系)
Step 4 : 通过 create 获取对应的 method ,反射调用接口

Step 1 : 核心注解的扫描

C02- AnnotationsConfigurator
	M101- void configure(Container container)
	M102- Set<Class<?>> getClasses(Container container)


// 此处就是 Bean 的加载注入
C03- DwrClassPathBeanDefinitionScanner
    M301- postProcessBeanFactory
    	FOR- ConfigurableListableBeanFactory.getBeanDefinitionNames() : 获取了ConfigurableListableBeanFactory下的所有的 BeanDefinition
    		- 判断是否包含 RemoteProxy / GlobalFilter 等注解
    			?-PS :这里应该可以优化 , 记得Spring 是可以直接通过注解获取 BeanDefinition 的
             IF- 如果包含 RemoteProxy
				- beanDefinitionClass.getSimpleName() : 实际上就是类名
				- 调用 M302:registerCreator(beanDefinitionHolder, beanDefinitionRegistry, beanDefinitionClass, javascript) 
             IF- 如果包含 GlobalFilter
				- filters.add(new RuntimeBeanReference(beanName)) : 添加远程 Filter   
	M302- registerCreator
		- BeanDefinitionBuilder.rootBeanDefinition(BeanCreator.class) : 准备了一个创造者 BeanDefinitionBuilder.BeanCreator
		- BeanDefinitionBuilder.rootBeanDefinition(CreatorConfig.class) : 准备了一个创造者 BeanDefinitionBuilder.CreatorConfig
		- 获取 Bean 中的 RemoteMethod 
		- 处理 Filter 注解 , 
		- 将 RemoteMethod 和 Filter 放入 BeanDefinitionBuilder.CreatorConfig
		- 准备 BeanDefinitionHolder , 并且完成 注册
    
    - 加载 beanDefinitionClass , 再获取其内部包含指定注解的bean
              

复制代码

Step 3 : 请求的拦截调用

当我们访问一个 DWR 请求的时候 , 会被 DwrServlet 所拦截

C04- DwrServlet
	M401- init
		- container = createContainer(servletConfig) : 
			- StartupUtil.createAndSetupDefaultContainer(servletConfig) 
                ?- 创建一个容器 ,深入的就不详述了 , 这里会反射到我们继承的 SpringDwrServlet
		- container.getBean(WebContextBuilder.class) : 获得一个 WebContextBuilder
		- webContextBuilder.engageThread(container, null, null) :??
		- configureContainer(container, servletConfig)
	M403- doPost : doPost 调用
		- webContextBuilder.engageThread(container, request, response);
		- UrlProcessor processor = container.getBean(UrlProcessor.class);
		- processor.handle(request, response) :详见 M501 , 处理请求

C05- UrlProcessor
    M501- handle
    	- request.getPathInfo() : /call/plaincall/Demo2Service.echo.dwr
    	- request.getContextPath() : ""
        FOR- Entry<String, Handler> entry : urlMapping.entrySet() : urlMapping 进行循环判断
            - Handler handler = entry.getValue() : 获取handler
            - handle(handler, request, response) : 执行 Handler
				?- 到这里执行的方式基本上已经清楚了 , 我们来找一下最后的代理方法就行

C06- CreatorModule
	M601- executeMethod
            ?- 这里就可以看到具体的 Method 执行逻辑
            - creator.getType().getMethod(methodDecl.getName(), methodDecl.getParameterTypes()) : 获取 Method
            - AjaxFilterChain 调用执行
            - return chain.doFilter(object, method, parameters) : 返回结果
     
            
AjaxFilterChain chain = new AjaxFilterChain(){
	public Object doFilter(Object obj, Method meth, Object[] params) throws Exception{
		// ....
		Method m = obj.getClass().getMethod(meth.getName(), meth.getParameterTypes());
		return m.invoke(obj, params);
	}
};

Object reply = chain.doFilter(object, method, parameters);
return reply;    

复制代码

剩下一个 , Method 是怎么管理的

C07- DefaultRemoter
    ?- 可以看到中间经过了这样一个类 , 其中有一个 Call 对象
    M701- execute(Call call)
    	?- call 主要是之前的请求属性生成的 , 这里就不细说了
    	- Module module = moduleManager.getModule(call.getScriptName(), true);
    	- MethodDeclaration method = call.getMethodDeclaration();
C08- BaseCallHandler
    ?- 这里就是通过 batch 生成了 calls 
    M- handle
        - CallBatch batch = new CallBatch(request)
        - Calls calls = marshallInbound(batch);
        - Replies replies = remoter.execute(calls);
    M- marshallInbound
    	-  call.findMethod(moduleManager, converterManager, inctx, callNum) : 此处查找 method , 详见 701
        
C07- Call
	M701- findMethod
        Module module = moduleManager.getModule(scriptName, true);
        List<MethodDeclaration> allMethods = new ArrayList<MethodDeclaration>();
        allMethods.addAll(Arrays.asList(module.getMethods()));

C08- CreatorModule
    ?- Module 管理
		- Class<?> creatorType = creator.getType()
		- Method[] methods = creatorType.getMethods()

复制代码

CreatorModule 的管理逻辑

到这里 Method 的逻辑就清楚了 , 最后一步 , CreatorModule 的管理逻辑


C09- DefaultCreatorManager
    ?- 核心管理类
    ?- SpringConfigurator.configure 中完成了创建

DwrServlet.init
DwrSpringServlet.configureContainer
    - StartupUtil.configure(container, configurators);
StartupUtil.configure
    - configurator.configure(container);
SpringConfigurator.configure
    ?- 这个类里面是总得管理类 , 没必要深入了
    -  creatorManager.addCreator(creator);

复制代码

五 . 扩展

如何通过 DWR 实现扫码 :

Step 1 : 前端生成一个 id 向后端申请二维码
Step 2 : 后端通过ID等其他本地信息生成二维码提供给前端 (二维码中包含 : redirect_uri / 认证信息)
Step 3 : App 扫描 二维码 , 通过 认证信息认证 判断后 , 调用 redirect_uri回调
Step 4 : 后端拿到回调信息 , 其中会包含 Step 1 的ID , 通过 ID 来告知对应前端 (当然也可以群发前端自己认证)

总结

关于使用:

DWR 源码看完后 ,感觉逻辑并不复杂 , 像个容器管理框架一样 ,去代理和反射了一套类 ,通过其本身的一套请求路径机制 , 来调用本地的方法 . 耦合性颇高.

关于历史:

dwr 的使用其实和 rpc 有在'说黑话'的思路上是一致的 , 都隐藏了网络层的具体逻辑 , 但是 rpc 仍然被 dubbo 作为主流 , 而dwr 却逐渐从大众视野里消失

想了一下 ,大概有以下几个原因 :

  • DWR的发布周期慢
  • 面对现在越来越复杂的网络环境 , dwr 的耦合性还是太强了 , 不便于开发和接口控制.
    • 尤其是近年来发展了微服务 ,这种初期看起来很简单的直接调用带了了非常多的问题
  • RPC 是为了解决更多的请求问题 ,它比 http 精简 , 保密 , 使调用简单化 , 而 DWR , 看多年前的吐槽就能发现 , 它会导致所有的 HTTP 请求被沾满 , 用法简单 ,但是使底层更加复杂
  • rpc 是服务端互调 , 其要解决的是服务端互调的复杂性
  • 前后端互调 , 本身就需要通过复杂性来控制业务能力 , 尤其是业务越来越复杂 , 其出发点主要集中在了简化 , 而没有提高可用性

总得来说 , 就是不够轻量级 , 比如看这些图 :

2009 年的时候 , 用 DWR 比 AJAX 更简单

image-20210409101941884.png

2015 年 , 大家都认为它太笨重了

image-20210409102109341.png

2020+ , JQuery 的笨重都被diss 到哪了 , 更轻量级的工具更受欢迎~~~

大人 , 时代变了!!!