2021SC@SDUSC
一、概述
在之前的博客中,我们分析了老年健康管理系统中的部分工具类,以及jwt相关的类,本篇博客继续分析之前未分析的工具类,为项目收尾做准备。
本篇博客计划分析一下工具类
关于Http请求的HttpHelper和HttpUtils,关于知识图谱树的TreeUtil和TreeNode(这个在之前的博客中有所涉及,本次是更详细的分析),另外还有UUIDUtils,以及其他工具类。
在本次阅读源码的过程中,关于请求和UUID的部分遇到了一些问题,这个过程中通过小组讨论,成功解决了相关的问题。
二、源码分析
2.1 HttpHelper类
本类是对HTTP工具的封装类,便于在其他类中调用
首先我们看到该类的引入,其中最关键的引入是ServletRequest,这个类用于获取InputStream,在后续的分析中我们可以看到
import javax.servlet.ServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
在接下来的方法中,我们可以看到该类的一个方法(本类只有一个方法,是一个静态方法)
- 首先创建StringBuilder对象,这个对象用于拼接字符串,在java中,如果涉及字符串的频繁拼接,一般不建议直接使用String对象,而应使用StringBuilder对象
- 之后通过request.getInputStream()获取请求输入流
- 通过while循环读取对应的数据,将对应的字符串保存在StringBuilder对象中
- 在finally语句中,
public static String getBodyString(ServletRequest request) {
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try (InputStream inputStream = request.getInputStream()) {
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
log.warn(e.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
return sb.toString();
}
2.2 HttpUtils类
在上面的分析中,我们分析了HttpHelper类,该类主要利用ServletRequest类获取InputStream,并获取请求中对应的数据,本工具类是对上面类的提升和拓展
首先我们看到该类的引入
- 在该类的引入中,我们可以看到,引入了java.net包下的大量内容,并且,引入了URL类,URLConnection类,这几个类是后续方法中进行网路连接的重点,
- 本类还引入了日志类,如Logger和LoggerFactory(工厂)
import javax.net.ssl.*;
import java.io.*;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.security.cert.X509Certificate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
接下来我们看到该类的属性
private static final Logger log = LoggerFactory.getLogger(HttpUtils.class);
这里使用设计模式中的工厂模式,使用LoggerFactory的getLogger方法获取对应的日志对象,后续方法中,会使用这个日志对象进行打印输出。
接下来我们看到该类的第一个方法
/**
* 向指定 URL 发送GET方法的请求
*
* @param url 发送请求的 URL
* @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @return 所代表远程资源的响应结果
*/
public static String sendGet(String url, String param)
{
return sendGet(url, param, CommonConstants.UTF8);
}
可以看到,在该类的这个静态的sendGet方法中,传入了url和param两个参数,并且调用了该类的另一个方法(该方法的重载),并且传入了字符编码为UTF-8
通过阅读源码我们知道,在CommonConstants类中,保存了很多的常量(项目中需要用到的和常量),其中就有UTF-8这个常量,另外,还有GBK这个常量
public class CommonConstants {
/**
* UTF-8 字符集
*/
public static final String UTF8 = "UTF-8";
/**
* GBK 字符集
*/
public static final String GBK = "GBK";
}
这里仅摘取了部分变量
接下来我们看到另一个send方法
- 首先声明了一个StringBuilder对象和一个BufferedReader对象,第一个对象用于拼接字符串,另一个用于读取请求传来的数据(拼接字符串的部分与上一个方法类似,这里不再赘述)
- 创建一个URL对象,并传入对应的URLString
- 通过这个URL对象的openConnection方法,得到一个URLConnection对象,这个连接对象中保存了连接的相关信息
- 向URLConnection对象中设置相关的属性值,并调用connect对象连接
- 通过URLConnection对象的getInputStream方法获取流对象,在通过while循环读取通过请求获取到的数据
- 接下来是对各种异常的处理,在finally语句中释放资源
- 调用StringBuilder的toString方法,返回读取到的数据
/**
* 向指定 URL 发送GET方法的请求
*
* @param url 发送请求的 URL
* @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @param contentType 编码类型
* @return 所代表远程资源的响应结果
*/
public static String sendGet(String url, String param, String contentType)
{
StringBuilder result = new StringBuilder();
BufferedReader in = null;
try
{
String urlNameString = url + "?" + param;
log.info("send : {}", urlNameString);
URL realUrl = new URL(urlNameString);
URLConnection connection = realUrl.openConnection();
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.connect();
in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType));
String line;
while ((line = in.readLine()) != null)
{
result.append(line);
}
log.info("receive : {}", result);
}
catch (ConnectException e)
{
log.error("ConnectException, url=" + url + ",param=" + param, e);
}
catch (SocketTimeoutException e)
{
log.error("SocketTimeoutException, url=" + url + ",param=" + param, e);
}
catch (IOException e)
{
log.error("IOException, url=" + url + ",param=" + param, e);
}
catch (Exception e)
{
log.error("Exception, url=" + url + ",param=" + param, e);
}
finally
{
try
{
if (in != null)
{
in.close();
}
}
catch (Exception ex)
{
log.error("Exception, url=" + url + ",param=" + param, ex);
}
}
return result.toString();
}
接下来是关于post请求的方法
- 由于该方法发送的是post请求,而post请求传递参数不能使用queryString的形式
- 该方法使用PrintWriter对象发送对应的参数
- PrintWriter对象的创建时,传入conn.getOutputStream()作为对应的参数
- 通过out.print和out.flush方法发送对应的数据
- 其他内容与get请求类似
/**
* 向指定 URL 发送POST方法的请求
*
* @param url 发送请求的 URL
* @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @return 所代表远程资源的响应结果
*/
public static String sendPost(String url, String param)
{
PrintWriter out = null;
BufferedReader in = null;
StringBuilder result = new StringBuilder();
try
{
String urlNameString = url;
log.info("sendPost : {}", urlNameString);
URL realUrl = new URL(urlNameString);
URLConnection conn = realUrl.openConnection();
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setRequestProperty("contentType", "utf-8");
conn.setDoOutput(true);
conn.setDoInput(true);
out = new PrintWriter(conn.getOutputStream());
out.print(param);
out.flush();
in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
String line;
while ((line = in.readLine()) != null)
{
result.append(line);
}
log.info("receive : {}", result);
}
catch (ConnectException e)
{
log.error("ConnectException, url=" + url + ",param=" + param, e);
}
catch (SocketTimeoutException e)
{
log.error("SocketTimeoutException, url=" + url + ",param=" + param, e);
}
catch (IOException e)
{
log.error("IOException, url=" + url + ",param=" + param, e);
}
catch (Exception e)
{
log.error("Exception, url=" + url + ",param=" + param, e);
}
finally
{
try
{
if (out != null)
{
out.close();
}
if (in != null)
{
in.close();
}
}
catch (IOException ex)
{
log.error("Exception, url=" + url + ",param=" + param, ex);
}
}
return result.toString();
}
接下来看到下一个方法,通过方法名可以知道,该方法的主要作用是发送SSL连接的POST请求
- 可以看到该方法首先创建了一个StringBuilder对象,之后又通过拼接字符串的方法得到对应的url。
- 通过SSLContext的getInstance方法获取SSLContext对象,这个getInstance方法用于放回对象
- 之后对该对象进行初始化操作,调用init方法
- 之后获取连接对象HttpsURLConnection,通过HttpsURLConnection对象的setSSLSocketFactory方法,将SSLContext的Factory对象传入
- 之后通过输入流读取对应的数据,利用while循环
- 在循环中,原本的编码是ISO-8859-1,需要转换为UTF-8
- 在catch语句块中处理可能抛出的各种异常,并在finally语句块中释放资源
public static String sendSSLPost(String url, String param)
{
StringBuilder result = new StringBuilder();
String urlNameString = url + "?" + param;
try
{
log.info("sendSSLPost : {}", urlNameString);
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());
URL console = new URL(urlNameString);
HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setRequestProperty("contentType", "utf-8");
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setSSLSocketFactory(sc.getSocketFactory());
conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
conn.connect();
InputStream is = conn.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String ret = "";
while ((ret = br.readLine()) != null)
{
if (ret != null && !ret.trim().equals(""))
{
result.append(new String(ret.getBytes("ISO-8859-1"), "utf-8"));
}
}
log.info("receive : {}", result);
conn.disconnect();
br.close();
}
catch (ConnectException e)
{
log.error("ConnectException, url=" + url + ",param=" + param, e);
}
catch (SocketTimeoutException e)
{
log.error("SocketTimeoutException, url=" + url + ",param=" + param, e);
}
catch (IOException e)
{
log.error("IOException, url=" + url + ",param=" + param, e);
}
catch (Exception e)
{
log.error("Exception, url=" + url + ",param=" + param, e);
}
return result.toString();
}
在这里需要简单介绍一下SSL,SSL的全称是Secure Sockets Layer(安全套接字协议),是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层与应用层之间对网络连接进行加密。
SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。
上面是SSL客户端与服务器的握手过程
2.3 树节点及建树过程
在之前的博客中,我曾介绍过老年健康知识图谱中关于知识树的节点和建树过程,这次我们需要更详细地分析一下建树的过程,以及树节点(树节点不仅保存了子节点,还保存了父节点),上次的博客仅分析了子节点的情况
可以看到,树节点类保存了id,父节点id,以及子节点,子节点用链表保存,同样是TreeNode对象,里面提供了get和set方法,其中关于子节点的set方法,使用了List的add方法
public class TreeNode {
protected int id;
protected int parentId;
protected List<TreeNode> children = null;
public List<TreeNode> getChildren() {
return children;
}
public void setChildren(List<TreeNode> children) {
this.children = children;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getParentId() {
return parentId;
}
public void setParentId(int parentId) {
this.parentId = parentId;
}
public void add(TreeNode node){
children.add(node);
}
}
在构建树的TreeUtil类中,我们可以看到,本类中使用了范型T,在bulid方法中,T要求继承TreeNode类,而TreeNode类中的各个属性都是protected,子类可以继承。在该类中,可以看到,建树的方法一共有两个,一个是两层循环建树,另一个是递归建树
- 在build方法中,通过foreach循环创建树
- 首先判断是否为根节点,如果是,就加入树中,树的结构用一个链表存储
- 如果子节点为空,则创建一个空的ArrayList
- 在递归建树方法中,调用findChildren方法,而findChildren方法中再次调用自身,实现递归建树。
- 当子节点为空时,递归逐层返回
public class TreeUtil{
/**
* 两层循环实现建树
*
* @param treeNodes 传入的树节点列表
* @return
*/
public static <T extends TreeNode> List<T> bulid(List<T> treeNodes,Object root) {
List<T> trees = new ArrayList<T>();
for (T treeNode : treeNodes) {
if (root.equals(treeNode.getParentId())) {
trees.add(treeNode);
}
for (T it : treeNodes) {
if (it.getParentId() == treeNode.getId()) {
if (treeNode.getChildren() == null) {
treeNode.setChildren(new ArrayList<TreeNode>());
}
treeNode.add(it);
}
}
}
return trees;
}
/**
* 使用递归方法建树
*
* @param treeNodes
* @return
*/
public static <T extends TreeNode> List<T> buildByRecursive(List<T> treeNodes,Object root) {
List<T> trees = new ArrayList<T>();
for (T treeNode : treeNodes) {
if (root.equals(treeNode.getParentId())) {
trees.add(findChildren(treeNode, treeNodes));
}
}
return trees;
}
/**
* 递归查找子节点
*
* @param treeNodes
* @return
*/
public static <T extends TreeNode> T findChildren(T treeNode, List<T> treeNodes) {
for (T it : treeNodes) {
if (treeNode.getId() == it.getParentId()) {
if (treeNode.getChildren() == null) {
treeNode.setChildren(new ArrayList<TreeNode>());
}
treeNode.add(findChildren(it, treeNodes));
}
}
return treeNode;
}
}
2.4 关于UUID
在老年健康知识图谱系统中,由于需要产生唯一标识,因此封装了UUID相关的工具类。该UUID工具类的代码如下
可以看到,这个类中有一个String类型的数组,里面保存了26个字母,在之后生成uuid的方法中会使用。
public static String[] chars = new String[] { "a", "b", "c", "d", "e", "f",
"g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s",
"t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I",
"J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
"W", "X", "Y", "Z" };
该方法主要用于生成简单的uuid
- 在该方法中,首先创建了一个StringBuilder对象
- 接着,通过java.util.UUID类的randomUUID方法生成对应的uuid,并将-替换成空字符串
- 之后利用for循环,对我们得到的uuid进行修改
- 使用Integer.parseInt(str, 16)将字符串转为16进制数
- 使用StringBuilder的append方法连接生成的字符串
public class UUIDUtils {
public static String generateShortUuid() {
StringBuffer shortBuffer = new StringBuffer();
String uuid = UUID.randomUUID().toString().replace("-", "");
for (int i = 0; i < 8; i++) {
String str = uuid.substring(i * 4, i * 4 + 4);
int x = Integer.parseInt(str, 16);
shortBuffer.append(chars[x % 0x3E]);
}
return shortBuffer.toString();
}
}
因此可见,该方法的主要目的是,对java.util.UUID类中生成的randomUUID进行修改,修改成短uuid,由于for循环只循环8次,因此生成的是8位的短uuid。
三、总结
在本次的源码分析中,通过与小组成员和老师的交流,我成功地分析了这部分代码。总的来说,通过这一部分代码的阅读,对于项目中请求发送,建树过程,生成8位短uuid等有了更深入的了解,希望在之后的项目实训中,能用上本项目的一些设计思想。同时也感谢小组成员和老师对我的帮助。