一、 项目介绍
1. 背景
根据IP得到位置,加标签
进行大数据分析,比如淘宝推荐等提供优质数据
www.ip.cn 等 查询IP
2. 需求
IP 分析 归属地信息 , 查找在毫秒内完成
IP地址库,公网都是开放的
IANA : 国际组织,负责公网IP维护分发
3. 技术栈
Eclipse ,JavaSE中面向对象、IO流、二分法算法、Base64编码、工具类封装
4. 目标
通过开发IP地址归属地查询平台,我们需要对JavaSE综合技术有所提升,增强实战能力。学习完该项目我们应该具备如下能力:
1 面向对象程序设计
2 工具类封装与使用写法
3 文件IO流
4 字符串处理
5 二分法查找
6 IP地址的不同形式的使用
二、 代码优化
1. BUG优化
目前用户输入的IP地址.如果格式不正确,就会出现下标越界异常
所以我们需要添加校验 : 正则表达式
1.1 技术问题
加入正则表达式
Java类中两个核心类
Pattern和Matcher
Pattern 是正则表达式引擎
Matcher 是功能较强大的匹配器
三种匹配模式
Matches : 全词匹配
Find : 任意位置
lookingAt : 从前往后匹配
Find和group连用可以提取数据
public class TestRegex_01 {
public static void main(String[] args) {
// (?=(\b|\D))(((\d{1,2})|(1\d{1,2})|(2[0-4]\d)|(25[0-5]))\.){3}((\d{1,2})|(1\d{1,2})|(2[0-4]\d)|(25[0-5]))(?=(\b|\D))
// [^0-9]
// . 匹配任意字符
// \ 转移符
// + 大于等于1
// * 大于等于0
// ? 0或1
// {n} n次
// {n,} 大于等于n次
// {n,m} 大于等于n且小于等于m
String regex = "\\d{11}";
String input = "a13101354234a";
// 创建引擎
Pattern pattern = Pattern.compile(regex);
// 创建匹配器
Matcher matcher = pattern.matcher(input);
// 匹配校验
// 全词匹配
// System.out.println(matcher.matches());
// 从前往后匹配
// System.out.println(matcher.lookingAt());
// 任意位置
System.out.println(matcher.find());
regex = "(?=(\\b|\\D))(((\\d{1,2})|(1\\d{1,2})|(2[0-4]\\d)|(25[0-5]))\\.){3}((\\d{1,2})|(1\\d{1,2})|(2[0-4]\\d)|(25[0-5]))(?=(\\b|\\D))";
input = "352.35.4.6";
pattern = Pattern.compile(regex);
matcher = pattern.matcher(input);
System.out.println(matcher.matches());
}
}
1.2 封装方法
/**
* 格式校验 全词匹配
*
* @param regex
* @param input
* @return
*/
public static boolean isValid(String regex, String input) {
// 创建引擎对象
Pattern pattern = Pattern.compile(regex);
// 创建匹配器
Matcher matcher = pattern.matcher(input);
// 进行全词校验
return matcher.matches();
}
}
1.3 测试
public class TestRegex_02 {
public static void main(String[] args) {
String regex = "(?=(\\b|\\D))(((\\d{1,2})|(1\\d{1,2})|(2[0-4]\\d)|(25[0-5]))\\.){3}((\\d{1,2})|(1\\d{1,2})|(2[0-4]\\d)|(25[0-5]))(?=(\\b|\\D))";
String input = "252.35.4.6";
System.out.println(RegexUtil.isValid(regex, input));
}
}
1.4 应用到业务当中
public class RegexUtil {
/**
* 校验IP
*
* @param input
* @return
*/
public static boolean isValidIP(String input) {
String regex = "(?=(\\b|\\D))(((\\d{1,2})|(1\\d{1,2})|(2[0-4]\\d)|(25[0-5]))\\.){3}((\\d{1,2})|(1\\d{1,2})|(2[0-4]\\d)|(25[0-5]))(?=(\\b|\\D))";
return isValid(regex, input);
}
对外提供的接口为DataProcessManager类中的getLocation方法,并不是入口类
所以需要在getLocation方法中加入正则校验
/**
* 对外提供的接口,入参是IP,出参是归属地
*
* @param ip
* @return
*/
public static String getLocation(String ip) {
// 校验IP
if (!RegexUtil.isValidIP(ip)) {
return "请输入正确的IP地址";
}
1.5 测试
尽管IP格式不正确,也不会像一开始一样,.终止程序生命周期了,使程序的鲁棒性得到增强
2. 性能调优
2.1 相关技术
2.1.1 硬件方面
内存,CPU,磁盘,网络 等 都可以实现调优
2.1.2 软件方面
2.1.2.1 直接调优
哪里慢就调哪里,不需要一些其他花哨的
主要指算法和内存层面的调优,一般难度较大,所以绝大部分都是非直接调优
2.1.2.2 迂回调优
通过一些设计和策略进行优化
2.1.2.3 迂回调优方向
缓存策略
通过时间断点可以看出,结构化耗时较多
一开始,同时校验两个IP的时候,需要读取数据两次,结构化数据两次,所以我们把读数据和结构化数据加入到静态语句块中,从而可以让第二次及以后进行查询的时候,不再进行结构化操作,而是使用结构好的数据,这样效率得到了提高
那么如果让每一次生命周期都使用的是结构化好的数据,那么效率应该还会有所提高
序列化和反序列化
2.2 首次调优
2.2.1 技术问题
序列化 : 把内存中的数据持久化存储到硬盘当中
反序列化 : 把存储到本地文件中的数据,加载到内存中
想要被序列化,该类必须实现Serlizable接口
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public User(String name, int age) {
super();
this.name = name;
this.age = age;
}
public class TestSerDe_01 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
User user = new User("张三", 18);
String filePath = "obj.data";
// 1 序列化
FileOutputStream fos = new FileOutputStream(filePath);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(user);
oos.flush();
oos.close();
// 2 反序列化
FileInputStream fis = new FileInputStream(filePath);
ObjectInputStream ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
System.out.println(obj);
ois.close();
}
}
2.2.2 封装工具类
/**
* 序列化相关工具类
*
* @author 天亮教育-帅气多汁你泽哥
* @Date 2022年2月12日 下午2:12:48
*/
public class SerDeUtil {
/**
* 序列化
*
* @param obj
* @param filePath
* @throws IOException
*/
public static void saveObj(Object obj, String filePath) throws IOException {
// 1 序列化
FileOutputStream fos = new FileOutputStream(filePath);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
oos.flush();
oos.close();
}
/**
* 反序列化
*
* @param filePath
* @return
* @throws IOException
* @throws ClassNotFoundException
*/
public static Object getObj(String filePath) throws IOException,
ClassNotFoundException {
// 2 反序列化
FileInputStream fis = new FileInputStream(filePath);
ObjectInputStream ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
return obj;
}
}
2.2.3 测试
public class TestSerDe_02 {
public static void main(String[] args) throws IOException,
ClassNotFoundException {
User user = new User("李四", 18);
String filePath = "obj.data";
// // 1 序列化
SerDeUtil.saveObj(user, filePath);
// // 2 反序列化
Object obj = SerDeUtil.getObj(filePath);
System.out.println(obj);
}
}
2.2.4 初始化优化
在初始化的时候,判断是否有序列化之后的文件
如果没有,就读取数据,结构化,并且序列化到本地
如果有序列化的文件,则不再读取文件结构化,而且进行反序列化直接获取排序之后的数据
所以 需要更改初始化的地方,也就是DataProcessManager中的静态代码块
注意 IPAndLocationPojo 需要实现 Serinalizable接口
public class DataProcessManager {
private static IPAndLocationPojo[] ipAndLocationPojoArray = null;
static {
// 序列化之后的文件名
String serdeObjFilePath = "ipLibObj.data";
// 1 文件路径
String ipLibrayPath = "ip_location_relation.txt";
String encoding = "UTF-8";
// 保存数据对象
List<IPAndLocationPojo> ipAndLocationPojos = null;
// 判断是否有序列化的文件
File file = new File(serdeObjFilePath);
if (file.exists()) {
// TODO
long startTime = System.currentTimeMillis();
// 如果存在 就反序列化
try {
Object obj = SerDeUtil.getObj(serdeObjFilePath);
ipAndLocationPojoArray = (IPAndLocationPojo[]) obj;
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("反序列化,耗时 : " + (endTime - startTime));
}else {
// 不存在 就读取数据 并且结构化 排序 然后 进行序列化
try {
// 获取数据
ipAndLocationPojos = DataProcessManager.getPojoList(ipLibrayPath,
encoding);
// 转数组并排序
// TODO
long startTime = System.currentTimeMillis();
ipAndLocationPojoArray = DataProcessManager
.convertListToArraySort(ipAndLocationPojos);
// 序列化
SerDeUtil.saveObj(ipAndLocationPojoArray, serdeObjFilePath);
long endTime = System.currentTimeMillis();
System.out.println("转数组并排序序列化,耗时 : " + (endTime - startTime));
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.2.5 测试结果
引入序列化后
首次执行 : 7000
第二次执行 : 19000
通过测试 是因为 序列化和反序列化降低了效率,也就是读写效率低
2.3 IO调优
2.3.1 引入缓冲流
public class SerDeUtil {
/**
* 序列化
*
* @param obj
* @param filePath
* @throws IOException
*/
public static void saveObj(Object obj, String filePath,int cacheByteArrayLength) throws IOException {
// 1 序列化
FileOutputStream fos = new FileOutputStream(filePath);
// 字节数组流
ByteArrayOutputStream baos = new ByteArrayOutputStream(cacheByteArrayLength);
ObjectOutputStream oos = new ObjectOutputStream(baos);
// 数据写出到字节数组中
oos.writeObject(obj);
// 转换为字节数组
byte[] byteArray = baos.toByteArray();
oos.flush();
oos.close();
fos.write(byteArray);
fos.close();
}
/**
* 反序列化
*
* @param filePath
* @return
* @throws IOException
* @throws ClassNotFoundException
*/
public static Object getObj(String filePath,int cacheByteArrayLength) throws IOException,
ClassNotFoundException {
// 2 反序列化
FileInputStream fis = new FileInputStream(filePath);
byte[] byteArray = new byte[cacheByteArrayLength];
// 数据读取到字节数组中
fis.read(byteArray);
fis.close();
// 字节数组缓冲流
ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
ObjectInputStream ois = new ObjectInputStream(bais);
Object obj = ois.readObject();
ois.close();
return obj;
}
}
2.3.2 调用处更改
2.3.3 测试结果
首次执行 : 因为需要读取,结构化,排序,序列化 所以耗时2000
其次 每次生命周期执行 只需要进行反序列化即可 , 耗时 : 900
2.4 代码标准化
2.4.1 编写静态数据类型
public class StaticValue {
// 序列化之后的文件名
public static String serdeObjFilePath = "ipLibObj.data";
// 地址库文件路径
public static String ipLibrayPath = "ip_location_relation.txt";
// 字符编码
public static String encoding = "UTF-8";
// 序列化文件大小
public static int cacheByteArrayLength = 25 * 1024 * 1024;
}
2.4.2 更改调用处
public class DataProcessManager {
private static IPAndLocationPojo[] ipAndLocationPojoArray = null;
static {
// 保存数据对象
List<IPAndLocationPojo> ipAndLocationPojos = null;
// 判断是否有序列化的文件
File file = new File(StaticValue.serdeObjFilePath);
if (file.exists()) {
// TODO
long startTime = System.currentTimeMillis();
// 如果存在 就反序列化
try {
Object obj = SerDeUtil.getObj(StaticValue.serdeObjFilePath,
StaticValue.cacheByteArrayLength);
ipAndLocationPojoArray = (IPAndLocationPojo[]) obj;
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("反序列化,耗时 : " + (endTime - startTime));
} else {
// 不存在 就读取数据 并且结构化 排序 然后 进行序列化
try {
// 获取数据
ipAndLocationPojos = DataProcessManager.getPojoList(
StaticValue.ipLibrayPath, StaticValue.encoding);
// 转数组并排序
// TODO
long startTime = System.currentTimeMillis();
ipAndLocationPojoArray = DataProcessManager
.convertListToArraySort(ipAndLocationPojos);
// 序列化
SerDeUtil.saveObj(ipAndLocationPojoArray,
StaticValue.serdeObjFilePath,
StaticValue.cacheByteArrayLength);
long endTime = System.currentTimeMillis();
System.out.println("转数组并排序序列化,耗时 : " + (endTime - startTime));
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.5 再次优化
2.5.1 IO优化
1 没有优化
首次执行 : 1700-1900
后续执行 : 1700-1900
2 引入序列化
文件大小是 38.3M
首次执行 : 6000-7000
后续执行 : 19000-20000
3 引入缓冲流
文件大小是 38.3
首次执行 : 2000
后续执行 : 800-900
通过测试 是因为 序列化写 和 反序列化读 慢
此时已经引入了缓冲流,进行了优化,下一步就要尝试优化文件大小
文件小,速度就快
transient 修饰符 修饰的属性不能被序列化
因为我们从始至终都没有使用startIP和endIP 所以 可以使用transient进行修饰
更改之后 序列化的文件 大小为 24.6
更改定义的文件大小
首次执行 : 2000
其次 : 450左右