2021SC@SDUSC
在上一篇文章中,我结束了openmeetings项目db模块的源码分析,完成了我负责的两个模块之一。接下来,我将开启openmeetings-core模块的分析。core模块,意为内核模块,也就是说这个模块的源码应该是与系统内核的处理相关,定义了一系列核心功能。
目录
core模块目录结构
util目录
IClientUtil.java
StrongPasswordValidator.java
WebSocketHelper.java
util/ws目录
WsMessageChat.java
WsMessageRoom.java
总结
core模块目录结构
同db模块一样,core模块下有src子目录、openmeetings-core.iml文件、pom.xml文件。其中pom.xml文件是配置文件,在之前的分析中已经介绍过;openm-core.iml文件是idea创建的模块文件,用于java应用开发,存储模块开发相关的信息及模块路径信息等等;src目录则是源码文件。
在src目录下,又存在main、site、test三个子目录。其中main是存放项目的java文件及资源,test是存放用来测试的java文件及其资源,site目录中保存了项目将要生成的各html文件等。因此,我们分析的目标仍然是main目录下的java文件。在src/main/java中,存在org.apache.openmeetings-core包,包中存在许多下面的包,如converter、data、documents、ldap、mail、notifier、remote、rss、service、util等,包中存放java文件,例如util包:
?如converter包:
可见,与db模块相比,整个模块的目录结构几乎是相同的。所以,对于core模块的分析也可以采取同样的思路,即分每个子目录进行分析。按照分析db的习惯,我们将首先开启作为工具的util目录的分析。
util目录
首先看util目录的结构:
在目录下存在一个子目录ws和三个java文件,ws子目录下又存在5个java文件。首先来分析外面的三个java文件。
IClientUtil.java
IClientUtil是对客户相关的工具。首先看引入的内容:
引入了三个类,第一个是经常见到的StreamClient类,后面两个则是较为陌生。
org.red5.server.api.IClient类,其源码大体如下:
可以看到,它的作用是负责获取客户与red5服务器的连接。其具体作用无需深究,了解意义即可。
再看IClientUtil类的定义:
类的字段只有一个,即枚举类型ConAttrs,其字段有omId、sharing、recordingId。再看类的方法:
所有的方法均是static方法,意味着直接通过类名调用。第一个方法是init方法,形参有IClient client、String uid、boolean sharing,返回值类型是void。在方法内部,对client的属性进行设置。所以init方法的目的是,对client的属性设置成本类的枚举类中的字段。
第二个方法是getId,形参是IClient client,返回类型是String。它是要获取client的id属性值,并且以字符串的形式返回。
第三个方法是isSharing,形参是IClient client,返回类型是boolean。它通过对比client的属性值是不是真,来判断client是不是可分享的。
第四、五个方法分别是getRecoringId和setRecordingId,是对client的recordingId进行get和set。
至此,IClientUtil.java分析完毕。它完成了对red5下IClient类的属性值的获取和设置。附上源码:
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License") + you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openmeetings.core.util;
import org.apache.openmeetings.db.entity.room.StreamClient;
import org.red5.server.api.IClient;
import org.red5.server.api.scope.IScope;
public class IClientUtil {
private enum ConAttrs {
omId
, sharing
, recordingId
}
public static void init(IClient client, String uid, boolean sharing) {
client.setAttribute(ConAttrs.omId.name(), uid);
client.setAttribute(ConAttrs.sharing.name(), sharing);
}
/**
* Id of {@link StreamClient} for this IConnection
*
* @param client - {@link IClient} to get if from
* @return - Id of {@link StreamClient} for this IConnection, or <code>null</code>
*/
public static String getId(IClient client) {
Object o = client.getAttribute(ConAttrs.omId.name());
return o instanceof String ? (String)o : null;
}
public static boolean isSharing(IClient client) {
return Boolean.TRUE.equals(client.getAttribute(ConAttrs.sharing.name()));
}
public static Long getRecordingId(IScope scope) {
return (Long)scope.getAttribute(ConAttrs.recordingId.name());
}
public static void setRecordingId(IScope scope, Long recordingId) {
scope.setAttribute(ConAttrs.recordingId.name(), recordingId);
}
}
StrongPasswordValidator.java
看字面意思,它是要对密码进行严格的验证。应用场景很有可能是在登录时,或者需要进行一些关乎很重要信息的操作时。
首先看引入的内容:
前面的两个都是util模块下OpenmeetingsVariables类下的getMinPasswdLength和getWebAppRootKey方法,都见过。接下来是两个经常见到的类,Locale和Map类。最后一部分,引入了db模块下的LabelDao、User类,还有一些org.apache.wicket.util下的一些类,以及validation相关的验证类,最后还有日志相关的类。
看类的定义:
首先StrongPasswordValidator类实现了IValidator接口,接口定义了validate方法,让实现类来实现验证功能。接着,它有四个字段,前面两个都不再多说。后面两个分别是final boolean web(不可修改,表面要验证的是不是web项目)和User u(用户)。
看定义的方法:
?
先看到的是构造器。第一个构造器调用了第二个构造器,而第二个构造器只是简单赋值,没有什么可说的。?继续看下面的方法:
在下面的方法中是对密码进行一系列确定。第一个noDigit方法是判断字符串password是不是没有数字,返回的是password为空或者password中不存在数字,判断方法是正则表达式。下面的noSymbol、noUpperCase、noLowerCase、badLength均是这样判断。再往下看:
下面的第一个方法是checkWord(String password,String word),是static方法,返回值是boolean。在方法体内,先判断word是不是空或者长度小于3,如果是则返回false。接着,对word进行每三个字符的扫描,判断password中是否含有这三个字符(忽略大小写),如果有则返回true,如果扫描完还没有则返回false。它的目的应该是检查password中是否存在某个关键词之类的。
再下面的方法是hasStopWords(String password),返回值也是boolean。在其中调用了checkWord方法,检查password中是否有login等特殊字符串。
再往下看:
重载了两个error方法,返回值均是void。第一个error调用了第二个error方法,所以看第二个。第二个error方法的形参是IValidatable<String> pass(String类型的IValidatable)、String key、Map<String,Object> params。在方法体内,分为项目是不是web进行操作。具体细节不需要展开,只要说明它的目的是对错误信息进行设置和存储即可。
最后的两个方法是validate和setUser。
在validate方法中就要进行综合的验证了,在其中调用了之前定义的badLength、noLowerCase、noUpperCase等方法,并且进行错误的收集。
最后的setUser只是个setter。
至此,StrongPasswordValidator类分配完毕。它的作用是完成对密码的一系列检验。附上其源码:
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License") + you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openmeetings.core.util;
import static org.apache.openmeetings.util.OpenmeetingsVariables.getMinPasswdLength;
import static org.apache.openmeetings.util.OpenmeetingsVariables.getWebAppRootKey;
import java.util.Locale;
import java.util.Map;
import org.apache.openmeetings.db.dao.label.LabelDao;
import org.apache.openmeetings.db.entity.user.User;
import org.apache.wicket.util.collections.MicroMap;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.validation.IValidatable;
import org.apache.wicket.validation.IValidator;
import org.apache.wicket.validation.ValidationError;
import org.red5.logging.Red5LoggerFactory;
import org.slf4j.Logger;
public class StrongPasswordValidator implements IValidator<String> {
private static final long serialVersionUID = 1L;
private static final Logger log = Red5LoggerFactory.getLogger(StrongPasswordValidator.class, getWebAppRootKey());
private final boolean web;
private User u;
public StrongPasswordValidator(final User u) {
this(true, u);
}
public StrongPasswordValidator(final boolean web, final User u) {
this.web = web;
this.u = u;
}
private static boolean noDigit(String password) {
return password == null || !password.matches(".*\\d+.*");
}
private static boolean noSymbol(String password) {
return password == null || !password.matches(".*[!@#$%^&*\\]\\[]+.*");
}
private static boolean noUpperCase(String password) {
return password == null || password.equals(password.toLowerCase(Locale.ROOT));
}
private static boolean noLowerCase(String password) {
return password == null || password.equals(password.toUpperCase(Locale.ROOT));
}
private static boolean badLength(String password) {
return password == null || password.length() < getMinPasswdLength();
}
private static boolean checkWord(String password, String word) {
if (Strings.isEmpty(word) || word.length() < 3) {
return false;
}
for (int i = 0; i < word.length() - 3; ++i) {
String substr = word.toLowerCase(Locale.ROOT).substring(i, i + 3);
if (password.toLowerCase(Locale.ROOT).indexOf(substr) > -1) {
return true;
}
}
return false;
}
private boolean hasStopWords(String password) {
if (checkWord(password, u.getLogin())) {
return true;
}
if (u.getAddress() != null) {
String email = u.getAddress().getEmail();
if (!Strings.isEmpty(email)) {
for (String part : email.split("[.@]")) {
if (checkWord(password, part)) {
return true;
}
}
}
}
return false;
}
private void error(IValidatable<String> pass, String key) {
error(pass, key, null);
}
private void error(IValidatable<String> pass, String key, Map<String, Object> params) {
if (web) {
ValidationError err = new ValidationError().addKey(key);
if (params != null) {
err.setVariables(params);
}
pass.error(err);
} else {
String msg = LabelDao.getString(key, 1L);
if (params != null && !params.isEmpty() && !Strings.isEmpty(msg)) {
for (Map.Entry<String, Object> e : params.entrySet()) {
msg = msg.replace(String.format("${%s}", e.getKey()), "" + e.getValue());
}
}
log.warn(msg);
pass.error(new ValidationError(msg));
}
}
@Override
public void validate(IValidatable<String> pass) {
if (badLength(pass.getValue())) {
error(pass, "bad.password.short", new MicroMap<String, Object>("0", getMinPasswdLength()));
}
if (noLowerCase(pass.getValue())) {
error(pass, "bad.password.lower");
}
if (noUpperCase(pass.getValue())) {
error(pass, "bad.password.upper");
}
if (noDigit(pass.getValue())) {
error(pass, "bad.password.digit");
}
if (noSymbol(pass.getValue())) {
error(pass, "bad.password.special");
}
if (hasStopWords(pass.getValue())) {
error(pass, "bad.password.stop");
}
}
public void setUser(User u) {
this.u = u;
}
}
WebSocketHelper.java
作为习惯于开发前端的人,我对websocket相对熟悉。它在服务端可以操作socket套接字,不但可以接收前端发来的消息,还可以主动向前端发送信息,可以用来向前端发通知等等。因此,WebSocketHelper应该是对客户端与服务端之间信息的交流提供工具。
直接看类的定义:
类一共有5个字段。第一个是司空见惯的log,后面的几个都定义了一系列static final的字符串变量,在后面作为标志或者信息。看类的方法:
首先看到的是static方法setScope,返回类型是JSONObject,形参是JSONObject o、ChatMessage m(发送的消息)、long curUserId。在方法体内,首先定义了两个空串scope、scopeName,然后判断m是要发送给用户还是房间。如果是用户,则根据curUserId与m对比,获取到发送的用户的id,然后对scope进行相应标志。同理,对发送给房间的消息也是这样。如果是同时发送给用户和房间,则把scope进行"ID_ALL"的标志。最终,将scope和scopeName封装到o中返回。所以,setScope方法就是根据其接收方的类型对发送的消息进行相应的标志。
再下面看到了:
这是getMessage方法,也是静态方法,返回类型是JSONObject,形参有User curUser、List<ChatMessage> list、BiConsumer<JSONObject,User> uFmt。方法内部代码比较多,但逻辑很清晰,就是把list中的每个消息封装成对象,然后放入对象数组中,最终返回一个包含了该数组的对象。也就是说,getMessage就是对获取的消息进行封装,便于以后的访问与提取。
再往下看,可以看到封装好的send方法,进行消息的发送:
send方法比较特殊的地方在于,它的形参里面包含了函数类型,这是我在以往的java代码中很少看到的。传入了函数对象,就可以在方法内部调用这个函数。在send方法内部,新建一个线程,在线程内部进行消息的发送,并且start这个线程。?再后面,定义了sendUser、sendRoom等方法,其中都调用了send方法,进行消息的发送。
所以,整体来看,WebSocketHelper类定义了消息的收发方法,供其他地方可调用。
附上源码:
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License") + you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openmeetings.core.util;
import static org.apache.openmeetings.core.remote.ScopeApplicationAdapter.getApp;
import static org.apache.openmeetings.util.OpenmeetingsVariables.getWebAppRootKey;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import org.apache.commons.lang3.time.FastDateFormat;
import org.apache.openmeetings.IApplication;
import org.apache.openmeetings.core.util.ws.WsMessageAll;
import org.apache.openmeetings.core.util.ws.WsMessageChat;
import org.apache.openmeetings.core.util.ws.WsMessageRoom;
import org.apache.openmeetings.core.util.ws.WsMessageRoomMsg;
import org.apache.openmeetings.core.util.ws.WsMessageUser;
import org.apache.openmeetings.db.entity.basic.ChatMessage;
import org.apache.openmeetings.db.entity.basic.Client;
import org.apache.openmeetings.db.entity.room.Room.Right;
import org.apache.openmeetings.db.entity.user.User;
import org.apache.openmeetings.db.manager.IClientManager;
import org.apache.openmeetings.db.util.FormatHelper;
import org.apache.openmeetings.db.util.ws.RoomMessage;
import org.apache.openmeetings.db.util.ws.TextRoomMessage;
import org.apache.openmeetings.util.ws.IClusterWsMessage;
import org.apache.wicket.Application;
import org.apache.wicket.protocol.ws.WebSocketSettings;
import org.apache.wicket.protocol.ws.api.IWebSocketConnection;
import org.apache.wicket.protocol.ws.api.registry.IWebSocketConnectionRegistry;
import org.apache.wicket.protocol.ws.api.registry.PageIdKey;
import org.apache.wicket.protocol.ws.concurrent.Executor;
import org.red5.logging.Red5LoggerFactory;
import org.slf4j.Logger;
import com.github.openjson.JSONArray;
import com.github.openjson.JSONObject;
public class WebSocketHelper {
private static final Logger log = Red5LoggerFactory.getLogger(WebSocketHelper.class, getWebAppRootKey());
public static final String ID_TAB_PREFIX = "chatTab-";
public static final String ID_ALL = ID_TAB_PREFIX + "all";
public static final String ID_ROOM_PREFIX = ID_TAB_PREFIX + "r";
public static final String ID_USER_PREFIX = ID_TAB_PREFIX + "u";
private static JSONObject setScope(JSONObject o, ChatMessage m, long curUserId) {
String scope, scopeName = null;
if (m.getToUser() != null) {
User u = curUserId == m.getToUser().getId() ? m.getFromUser() : m.getToUser();
scope = ID_USER_PREFIX + u.getId();
scopeName = u.getDisplayName();
} else if (m.getToRoom() != null) {
scope = ID_ROOM_PREFIX + m.getToRoom().getId();
o.put("needModeration", m.isNeedModeration());
} else {
scope = ID_ALL;
}
return o.put("scope", scope).put("scopeName", scopeName);
}
public static JSONObject getMessage(User curUser, List<ChatMessage> list, BiConsumer<JSONObject, User> uFmt) {
JSONArray arr = new JSONArray();
final FastDateFormat fullFmt = FormatHelper.getDateTimeFormat(curUser);
final FastDateFormat dateFmt = FormatHelper.getDateFormat(curUser);
final FastDateFormat timeFmt = FormatHelper.getTimeFormat(curUser);
for (ChatMessage m : list) {
String smsg = m.getMessage();
smsg = smsg == null ? smsg : " " + smsg.replaceAll(" ", " ") + " ";
JSONObject from = new JSONObject()
.put("id", m.getFromUser().getId())
.put("displayName", m.getFromName())
.put("name", m.getFromUser().getDisplayName());
if (uFmt != null) {
uFmt.accept(from, m.getFromUser());
}
arr.put(setScope(new JSONObject(), m, curUser.getId())
.put("id", m.getId())
.put("message", smsg)
.put("from", from)
.put("actions", curUser.getId() == m.getFromUser().getId() ? "short" : "full")
.put("sent", fullFmt.format(m.getSent()))
.put("date", dateFmt.format(m.getSent()))
.put("time", timeFmt.format(m.getSent()))
);
}
return new JSONObject()
.put("type", "chat")
.put("msg", arr);
}
public static void sendClient(final Client _c, byte[] b) {
if (_c != null) {
send(a -> Arrays.asList(_c), (t, c) -> {
try {
t.sendMessage(b, 0, b.length);
} catch (IOException e) {
log.error("Error while broadcasting byte[] to room", e);
}
}, null);
}
}
public static void send(IClusterWsMessage _m) {
if (_m instanceof WsMessageRoomMsg) {
sendRoom(((WsMessageRoomMsg)_m).getMsg(), false);
} else if (_m instanceof WsMessageRoom) {
WsMessageRoom m = (WsMessageRoom)_m;
sendRoom(m.getRoomId(), m.getMsg(), false);
} else if (_m instanceof WsMessageChat) {
WsMessageChat m = (WsMessageChat)_m;
sendRoom(m.getChatMessage(), m.getMsg(), false);
} else if (_m instanceof WsMessageUser) {
WsMessageUser m = (WsMessageUser)_m;
sendUser(m.getUserId(), m.getMsg(), false);
} else if (_m instanceof WsMessageAll) {
sendAll(((WsMessageAll)_m).getMsg(), false);
}
}
public static void sendRoom(final RoomMessage m) {
sendRoom(m, true);
}
private static void sendRoom(final RoomMessage m, boolean publish) {
if (publish) {
publish(new WsMessageRoomMsg(m));
}
log.debug("Sending WebSocket message: {} {}", m.getType(), m instanceof TextRoomMessage ? ((TextRoomMessage)m).getText() : "");
sendRoom(m.getRoomId(), (t, c) -> t.sendMessage(m), null);
}
public static void sendRoom(final Long roomId, final JSONObject m) {
sendRoom(roomId, m, true);
}
private static void sendRoom(final Long roomId, final JSONObject m, boolean publish) {
if (publish) {
publish(new WsMessageRoom(roomId, m));
}
sendRoom(roomId, m, null, null);
}
public static void sendRoom(ChatMessage m, JSONObject msg) {
sendRoom(m, msg, true);
}
private static void sendRoom(ChatMessage m, JSONObject msg, boolean publish) {
if (publish) {
publish(new WsMessageChat(m, msg));
}
sendRoom(m.getToRoom().getId(), msg
, c -> !m.isNeedModeration() || (m.isNeedModeration() && c.hasRight(Right.moderator))
, null);
}
public static void sendUser(final Long userId, final String m) {
sendUser(userId, m, true);
}
private static void sendUser(final Long userId, final String m, boolean publish) {
if (publish) {
publish(new WsMessageUser(userId, m));
}
send(a -> ((IApplication)a).getOmBean(IClientManager.class).listByUser(userId), (t, c) -> {
try {
t.sendMessage(m);
} catch (IOException e) {
log.error("Error while sending message to user", e);
}
}, null);
}
public static void sendAll(final String m) {
sendAll(m, true);
}
private static void sendAll(final String m, boolean publish) {
if (publish) {
publish(new WsMessageAll(m));
}
new Thread(() -> {
Application app = (Application)getApp();
WebSocketSettings settings = WebSocketSettings.Holder.get(app);
IWebSocketConnectionRegistry reg = settings.getConnectionRegistry();
Executor executor = settings.getWebSocketPushMessageExecutor(); // new NewThreadExecutor();
for (IWebSocketConnection c : reg.getConnections(app)) {
executor.run(() -> {
try {
c.sendMessage(m);
} catch (IOException e) {
log.error("Error while sending message to ALL", e);
}
});
}
}).start();
}
protected static void publish(IClusterWsMessage m) {
IApplication app = getApp();
new Thread(() -> {
app.publishWsTopic(m);
}).start();
}
protected static void sendRoom(final Long roomId, final JSONObject m, Predicate<Client> check, BiFunction<JSONObject, Client, String> func) {
log.debug("Sending WebSocket message: {}", m);
sendRoom(roomId, (t, c) -> {
try {
t.sendMessage(func == null ? m.toString() : func.apply(m, c));
} catch (IOException e) {
log.error("Error while broadcasting message to room", e);
}
}, check);
}
private static void sendRoom(final Long roomId, BiConsumer<IWebSocketConnection, Client> consumer, Predicate<Client> check) {
send(a -> ((IApplication)a).getOmBean(IClientManager.class).listByRoom(roomId), consumer, check);
}
private static void send(
final Function<Application, Collection<Client>> func
, BiConsumer<IWebSocketConnection, Client> consumer
, Predicate<Client> check)
{
new Thread(() -> {
Application app = (Application)getApp();
WebSocketSettings settings = WebSocketSettings.Holder.get(app);
IWebSocketConnectionRegistry reg = settings.getConnectionRegistry();
Executor executor = settings.getWebSocketPushMessageExecutor(); //new NewThreadExecutor();
for (Client c : func.apply(app)) {
if (check == null || check.test(c)) {
final IWebSocketConnection wc = reg.getConnection(app, c.getSessionId(), new PageIdKey(c.getPageId()));
if (wc != null && wc.isOpen()) {
executor.run(() -> consumer.accept(wc, c));
}
}
}
}).start();
}
public static class NewThreadExecutor implements Executor {
@Override
public void run(Runnable command) {
new Thread(() -> {
command.run();
}).start();
}
}
}
util/ws目录
刚才将util目录下的三个java文件分析结束了,接下来需要展开分析的是util目录下的ws子目录,其结构如下:
在ws目录下有5个java文件,每个java文件与不同的角色相关,下面来分析一下。
WsMessageChat.java
WsMessageChat显然是与聊天相关的。看引入的内容:
引入的内容包括了db模块下定义的实体ChatMessage,还有util模块下的NullStringer类和IClusterWsMessage接口。IClusterWsMessage接口比较简单,只是代表可序列化。NullStringer类是JSONStringer的子类,可以封装字符串。最后引入的是JSONObject,非常熟悉了。
看类的定义:
首先类实现了IClusterWsMessage接口。类体内定义了三个字段,分别是序列化Id、final ChatMessage m、final String msg,都比较容易理解。
它的构造器传入两个参数:ChatMessage m和JSONObject msg。其中,对this.msg赋值时,是通过new NullStringer()进行初始化,调用JSONObject对象的toString方法进行赋值。后面两个方法就是getter了。
WsMessageChat类并没有太多很难的内容,字段内只是简单地定义了消息实体ChatMessage和消息内容msg。源码都在图中,不再给出。
WsMessageRoom.java
WsMessageRoom类的定义结构与WsMessageChat几乎一样:
字段的内容和方法都如出一辙,因此就不再多提。包括ws目录下的其余的三个java文件:WsMessageRoomMsg.java、WsMessageUser.java、WsMessageAll.java的内容也完全一样,所以对它们的分析也直接跳过了。
总结
在本文中,我开启了core模块的源码分析,梳理了core的结构,并且完成了util子目录的分析。接下来的几篇文章里,我将会逐步完成core模块的源码分析,最后从整体上总结一下一学期中对db模块和core模块的分析探索。
|