操作手册 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    /**
* @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

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

1
2
3
4
5
6
7
8
9
10
11
12
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 : 远程方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@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应用程序上下文中加载配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码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的配置
  • 显式地指定要选取的配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@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应用程序中的活动会话列表发生更改时,将通知此接口的实现 , 即监听变化时处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码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 , 直接获取即可 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码<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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码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 做了这些事

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码
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 类实现的 :

1
2
3
4
5
6
7
8
java复制代码
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 : 核心注解的扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码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 所拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码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 是怎么管理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码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 的管理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码
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 到哪了 , 更轻量级的工具更受欢迎~

大人 , 时代变了!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%