2019.6月,我通過社招入職現在所工作的公司,理論上應該有三個月時間的試用期,只有試用期表現良好我纔有機會轉正,但因爲一次優化代碼過程中造了一個輪子,我獲得了一個月轉正的機會。
我是一個懶人,又特別喜歡琢磨,在工作的過程中我發現有一個模塊運行非常的慢,主要原因是在這個模塊種需要大量的進行數據庫操作,而目前公司業務擴大,在數據庫中已經有上億條數據,每次對這個表進行操作,都需要花費將近3S的時間,而實際上,整個流程走下來,程序也就花費4S的時間,所以我就特別想把這段代碼優化一下,將代碼的耗時降低下來,經過一個星期的努力,輪子的初版發佈,同事們用完之後都覺得不錯,然後老大就給我遞交了提前轉正申請,我工作一個月就順利轉正了。
現在將我當時造輪子的主要思路在這裏寫下來,希望能給你一點啓發。
1.輪子相關
首先,我們現在所說的輪子可不是汽車上的輪子,之所以叫“輪子”,是爲了更好的理解。
汽車輪子是圓的,這種圓形輪子已經被各界廣泛認可是比較好的結構,我們無論怎麼去做,也很難超越“圓輪子”,所以我們只能在“圓輪子”的基礎上去<重複發明輪子>,即是所說的造輪子。但是有一句話叫做“不要重複造輪子”,因爲無論我們怎麼努力,也很難去超越以前已經有的輪子,那我們爲什麼還要去造輪子呢?
1.只有我們自己才懂得我們的需求,你在編程中無論用什麼框架,總覺得跟業務不是很契合,甚至有時候框架很臃腫,但是因爲要用到其中一個內容而不得不導入一個很大的框架。
2.已有的框架太複雜,我們完全沒法掌握他的所有內容,出了bug後一頭霧水解決不了問題
3.公司要求,不可使用第三方框架(多見於數據保密的公司),但是程序中要反覆使用某一組件功能
4.找不到合適的輪子
5.想裝B
如果遇到以上問題,或許自己造輪子纔是一個好辦法。
2.前期準備
本文所講的,可能只是造輪子最基礎的教程了,所以知識儲備方面要求的不是很全面,如果有大佬看到這篇文章,請輕點噴,我絕對虛心接受批評。
2.1.基礎知識
總從上次發了那篇關於閱讀源碼的文章,總是有朋友問我“你好,請問我現在剛大一,看不懂源碼怎麼辦?”,每每遇到這種問題,我都很絕望,我在那篇文章中所講的內容,都是建立在已經有一定編程基礎的前提上,如果正在看這篇文章的你沒有接觸過編程,那現在可以先點一下關注,點贊,收藏,然後回去好好學習一下基礎再看這篇文章。
我們想造一個輪子,最起碼有一定的編程能力,如果要有一個標準,就是能夠獨自搭建一個項目。
本文中主要運用到的知識點有:註解、反射、多態
2.2.瞭解源碼
比如Jdk源碼和一些框架的源碼(沒有閱讀過源碼的話請點擊我),造輪子在某種層面上來說,就是寫一個框架,我們在沒有基礎的情況下,先看一些大佬寫好的框架源碼是很有幫助的,在我們自己的輪子中,可以模仿他們框架的結構。
2.3.不怕失敗
第一次造輪子絕對是一個艱難而又漫長的過程,你會一次次失敗,你需要經受住失敗帶來的對你信心和耐心的打擊,如果你無法堅持,還不如不要開始。
3.開幹
如果你看到這裏,相信你已經準備好了,那麼現在就開始吧!
3.1.想好需求
既然是造輪子,那麼總得先想好這個輪子的用途,我們我們假設一個需求:通過註解實現系統日誌輸出到文件
具體需求如下:
1.記錄註解注入的數據
2.日誌記錄應通過文件保存到系統中,路徑可配置,若無配置則選用默認配置
3.日誌記錄需要添加時間標籤
4.日誌文件名可在註解中設置
5.引入隊列傳遞日誌MSG
3.2.創建項目
爲了簡化過程,我們可以直接創建一個Maven項目,在你對底層有更好的理解之後,就可以用更好的架構。
新建一個LogUtil項目,項目架構如下
因爲只是一個簡單的示例,所以有很多的內容知識有思想,但是還沒有實現
3.2.一些常量
主要保存在Constants.java文件中
public interface Constants {
//等下要引入的配置文件名
String CONFIG_FILE_NAME = "yzlogconfig";
//配置文件中配置的日誌路徑
String CONFIG_LOG_PATH = "logpath";
//配置文件中配置的要掃描的,可能存在我們註解的路徑
String CONFIG_SACN_PATH = "scanpath";
//若未聲明某些信息,則使用以下默認值
//默認的我們的日誌信息前綴,對日誌信息做簡單描述
String DEFAULT_CONTENT_PREFIX = "注入值:";
//默認的日誌文件名(實際寫入時會在日誌文件名後加上日期標籤)
String DEFAULT_FILE_NAME = "log";
//日誌信息類型,處理消息時會用到
String MSG_TYPE_LOG = "log";
//默認的Linux系統下的日誌路徑
String LINUX_LOG_PATH = "/home/data/";
//默認的Windows系統下的日誌路徑
String WIN_LOG_PATH = "D:/winLog/data/";
}
3.3.加載配置
思想:給予用戶配置權限
框架是拿給別人用的,一定要給予用戶自主配置的權限
這裏要加載的是那些引入我們輪子的項目的配置
某個項目在引入我們的輪子的時候,他自己是應當有權限去自己設置一些東西的,比如我們這裏的文件處理路徑,是給了用戶權限去配置的。
我們規定配置文件的文件名爲yzlogconfig.xml,等下在代碼中也可以看到這個配置文件名的設置。
ConfigurationUtil
這個工具類主要用來加載配置信息,基礎工具類,這裏不再累述
import java.util.ResourceBundle;
public class ConfigurationUtil {
private static Object lock = new Object();
private static ConfigurationUtil config = null;
private static ResourceBundle rb = null;
private ConfigurationUtil(String filename) {
rb = ResourceBundle.getBundle(filename);
}
public static ConfigurationUtil getInstance(String filename) {
synchronized (lock) {
if (null == config) {
config = new ConfigurationUtil(filename);
}
}
return (config);
}
public String getValue(String key) {
String ret = "";
if (rb.containsKey(key)) {
ret = rb.getString(key);
}
return ret;
}
}
3.4.日誌記錄功能實現
這裏算是核心功能的一部分了,需要的工具類有:DateUtil(獲取日期)、SystemUtil(獲取當前系統的類型)、FileUtil(創建日誌文件)
DataUtil
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtil {
public final static String DATE_A = "yyyy-MM-dd";
public final static String DATE_B = "yyyy-MM-dd HH:mm:ss";
public final static String DATE_C = "yyyyMMddHHmmss";
public final static String DATE_D = "yyyyMMdd-HHmmss-SS";
public final static String DATE_E = "M月d日";
public final static String DATE_F = "MM-dd";
public final static String DATE_G = "yyyyMMddHHmmss";
// 普通的當前時間轉字符串方法,格式爲yyyy-MM-dd
public static String getDate() {
SimpleDateFormat sdf = new SimpleDateFormat(DATE_A);
return sdf.format(new Date());
}
public static String getDateTime() {
Date date = new Date();
String datestr;
SimpleDateFormat sdf = new SimpleDateFormat(DATE_B);
datestr = sdf.format(date);
return datestr;
}
}
SystemUtil
/**
*@描述 用於判斷當前系統
*@參數
*@返回值
*@創建人 Baldwin
*@創建時間 2020/4/4
*@修改人和其它信息
*/
public class SystemUtil {
/**
* 判斷系統時win還是linux
* @return
*/
public static boolean isLinux(){
String name = System.getProperty("os.name");
if(name.toLowerCase().startsWith("win"))
return false;
else
return true;
}
}
FileUtil
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
public class FileUtil {
// 在已經存在的文件後面追加寫的方式
public static boolean write(String path, String str) {
File f = new File(path);
File fileParent = f.getParentFile();
BufferedWriter bw = null;
try {
if(!fileParent.exists()){
fileParent.mkdirs();
}
if(!f.exists()){
f.createNewFile();
}
// new FileWriter(name,true)設置文件爲在尾部添加模式,參數爲false和沒有參數都代表覆寫方式
bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(path, true), "UTF-8"));
bw.write(str);
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
try {
if(bw!=null)bw.close();
} catch (Exception e) {
System.out.println("FileUtil.write colse bw wrong:" + e);
}
}
return true;
}
}
LogUtil
思想:默認配置
很多時候,我們的用戶可能沒有配置需要配置的信息,而且註解中也沒有聲明,那麼就要求我們存在默認的配置來填補這些空缺,從而避免由於空配置導致的錯誤
在這個類裏面,我們主要進行一些日誌路徑和內容的整理
import cn.yzstu.support.Constants;
import cn.yzstu.support.DateUtil;
import cn.yzstu.support.FileUtil;
import cn.yzstu.support.SystemUtil;
public class LogUtil {
//日誌寫入操作
public static void write2file(String path, String fileName, String content) {
//獲取當前日期,我們的日誌保存的文件夾名是自定義path+日期
String date = DateUtil.getDate()+"/";
try {
//傳了path,那我們直接用這個path
if (null != path && 0 != path.length()) {
//寫入
FileUtil.write(path + date + fileName + ".txt",
DateUtil.getDateTime() + ":" + content + "\r\n");
} else {
//沒有傳path或錯誤使用默認的路徑
if (SystemUtil.isLinux()) {
FileUtil.write(Constants.LINUX_LOG_PATH + date + fileName + ".txt",
DateUtil.getDateTime() + ":" + content + "\r\n");
} else {
FileUtil.write(Constants.WIN_LOG_PATH + date + fileName + ".txt",
DateUtil.getDateTime() + ":" + content + "\r\n");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.5.日誌消息
我們等一下要把日誌消息放到隊列裏來處理,這裏定義一個日誌類的消息類型,方便後續處理,在本示例中,做了簡化處理,實際上對於隊列消息,我們需要定義一個統一接口,讓所有的消息類型都實現他,這樣如果我們的消息類型很多的時候,就能做一個統一的管理了。
我們爲了方便處理,在構造函數中就讓這個消息入列了,並且把他的MsgType直接設置成了logmsg
import cn.yzstu.core.MsgQueue;
import cn.yzstu.support.Constants;
public class LogMsg {
private String path;
private String content;
private String fileName;
private String msgType;
public LogMsg(String path, String content, String fileName) {
this.path = path;
this.content = content;
this.fileName = fileName;
this.msgType = "logmsg";
//在構造函數中就讓這個消息入列
MsgQueue.push(this);
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getMsgType() {
return this.msgType;
}
public void setMsgType(String msgType) {
this.msgType = msgType;
}
@Override
public String toString() {
return "LogMsg{" +
"path='" + path + '\'' +
", content='" + content + '\'' +
", fileName='" + fileName + '\'' +
", msgType='" + msgType + '\'' +
'}';
}
}
3.6.定義隊列
如果對隊列不是很熟悉的,可以看我另一篇文章:細說隊列
實際情況中,我們的隊列裏面會存在很多中類型的消息,在本示例中只存在logmsg。
import cn.yzstu.beans.LogMsg;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
public class MsgQueue {
private static Queue<LogMsg> queue = new ConcurrentLinkedDeque<>();
//消息入列
public static boolean push(LogMsg logMsg){
return queue.offer(logMsg);
}
//消息出列
public static LogMsg poll(){
return queue.poll();
}
//消息隊列是否已經處理完畢,處理完畢返回true
public static boolean isFinash(){
return !queue.isEmpty();
}
}
3.7.定義註解
在此我們定義一個名爲YzLogWrite的註解類,它主要實現的功能是值注入及日誌標記。
對於註解不是很瞭解的同僚可以看我的另一篇文章:想自己寫框架?不瞭解註解可不行
YzLogWrite
import java.lang.annotation.*;
//作用於字段
@Target({ElementType.FIELD})
//運行時生效
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface YzLogWrite {
//需要註解的值
int value() default -1;
//默認是Linux系統,默認記錄文件夾如下
String path() default "";
//文件名
String fileName() default "";
//內容
String msgPrefix() default "";
}
3.8.註解邏輯實現
思想:聲明大於配置
如果我們在註解中聲明瞭一些用到的信息,但是配置文件中也有這些信息,我們應該有限選用註解中聲明的信息。
思想:自定義掃描路徑
我們應當給予用戶權限去讓他自己規定自己註解使用的包
我們定義了註解,但是還需要進行一些操作來完善註解的功能,在這一部分,我們要將值注入,並且將值信息發送到消息隊列中。
DealAnnotation
import cn.yzstu.annotation.YzLogWrite;
import cn.yzstu.beans.LogMsg;
import cn.yzstu.support.Constants;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
public class DealAnnotation {
//配置文件中設置的log所在地址
private static String LOG_PATH = ConfigurationUtil.getInstance(Constants.CONFIG_FILE_NAME).getValue(Constants.CONFIG_LOG_PATH);
//保存那些存在註解的class的類名
private List<String> registyClasses = new ArrayList<>();
public void injectAndMakeMsg() {
//需要掃描的註解可能存在的位置
String scanPath = ConfigurationUtil.getInstance(Constants.CONFIG_FILE_NAME).getValue(Constants.CONFIG_SACN_PATH);
doScanner(scanPath);
for (String className : registyClasses) {
try {
Class clazz = Class.forName(className);
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//獲取類的所有註解
Annotation[] annotations = field.getAnnotations();
//沒有註解或沒有我們的註解,跳過
if (0 == annotations.length || !field.isAnnotationPresent(YzLogWrite.class)) {
continue;
}
//獲取註解
YzLogWrite yzLogWrite = field.getAnnotation(YzLogWrite.class);
//提取註解中的值
//聲明大於配置
String path = null == yzLogWrite.path() || yzLogWrite.path().isEmpty() ? LOG_PATH : yzLogWrite.path();
String content = null == yzLogWrite.msgPrefix() || yzLogWrite.msgPrefix().isEmpty() ? Constants.DEFAULT_CONTENT_PREFIX : yzLogWrite.msgPrefix();
String fileName = null == yzLogWrite.fileName() || yzLogWrite.fileName().isEmpty() ? Constants.DEFAULT_FILE_NAME : yzLogWrite.fileName();
int value = yzLogWrite.value();
//新建logMsg,在構造函數中已入列
new LogMsg(path, content + ":" + value, fileName);
//開始注入
//強制訪問該成員變量
field.setAccessible(true);
//注入int值
field.setInt(Integer.class, value);
}
} catch (ClassNotFoundException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
private void doScanner(String scanPath) {
URL url = this.getClass().getClassLoader().getResource(scanPath.replaceAll("\\.", "/"));
File classPath = new File(url.getFile());
for (File file : classPath.listFiles()) {
if (file.isDirectory()) {
//如果是目錄則遞歸調用,直到找到class
doScanner(scanPath + "." + file.getName());
} else {
if (!file.getName().endsWith(".class")) {
continue;
}
String className = (scanPath.replace("/", ".") + "." + file.getName().replace(".class", ""));
registyClasses.add(className);
}
}
}
}
3.9.處理消息
思想:多態分發
儘量讓我們的隊列能夠處理不同種類的消息,我們在獲取到隊列中的消息之後,應當有一個對消息類型的判斷,並將不同類型的消息分發到不同方法中的操作
通過上面的操作,我們已經把值注入並且把日誌消息傳到隊列中去了,現在還要對隊列中的消息進行處理
import cn.yzstu.beans.LogMsg;
public class DealMsg extends Thread{
@Override
public void run() {
while (MsgQueue.isFinash()){
//多態
//實際中,我們可以定義很多中msg,用type來區分,並通過不同的方法來處理
//此處運用了這種思想,但是沒有實現具體操作
LogMsg logMsg = MsgQueue.poll();
switch (logMsg.getMsgType()){
case "logmsg" :
//如果類型是logmsg,那就通過日誌來處理
dealLogMsg(logMsg);
break;
default:defaultMethod(logMsg);
}
}
this.interrupt();
}
private void defaultMethod(LogMsg logMsg) {
System.out.println("no msg");
}
private void dealLogMsg(LogMsg logMsg) {
LogUtil.write2file(logMsg.getPath(),logMsg.getFileName(),logMsg.getContent());
}
@Override
public synchronized void start() {
this.run();
}
}
3.10.提供入口
我們的一個簡單的實例基本上功能已經完成了,那如何引入呢?這裏我採取的方法是留一個操作的方法來執行我們所有的功能。
import cn.yzstu.annotation.YzLogWrite;
public class StartWork {
//程序入口
public static void doWork(){
//處理:掃描註解、注入、發送日誌消息到隊列
new DealAnnotation().injectAndMakeMsg();
//創建線程來處理消息
new DealMsg().start();
}
}
4.測試
我們以上已經完成了所有的功能,需要現在就來測試一下。
創建配置類
我們規定配置文件名爲“yzlogconfig”,那麼現在在resource文件夾下創建一個該配置文件。
#logpath最後需要帶/
logpath = /opt/
scanpath = cn/yzstu/tt
我們只配置了log日誌路徑和註解位置,用以測試默認參數是否生效
創建測試類
我們在上面配置文件中規定了我們註解使用的包,所以應當在該包下去使用註解,否則掃描不到我們的註解
import cn.yzstu.annotation.YzLogWrite;
import cn.yzstu.core.StartWork;
public class Demo {
//因爲測試用的main函數是static,所以此時將age設置爲static
@YzLogWrite(value = 18,msgPrefix = "記錄Baldwin的年齡:")
static int age;
public static void main(String[] args) {
StartWork.doWork();
System.out.println(age);
}
}
執行結果
首先看控制檯,顯示注入成功
/opt/java/jdk1.8.0_241/bin/java -javaagent:/opt/jetbrains/idea-IU-193.6911.18/lib/idea_rt.jar=38115:/opt/jetbrains/idea-IU-193.6911.18/bin -Dfile.encoding=UTF-8 -classpath /opt/java/jdk1.8.0_241/jre/lib/charsets.jar:/opt/java/jdk1.8.0_241/jre/lib/deploy.jar:/opt/java/jdk1.8.0_241/jre/lib/ext/cldrdata.jar:/opt/java/jdk1.8.0_241/jre/lib/ext/dnsns.jar:/opt/java/jdk1.8.0_241/jre/lib/ext/jaccess.jar:/opt/java/jdk1.8.0_241/jre/lib/ext/jfxrt.jar:/opt/java/jdk1.8.0_241/jre/lib/ext/localedata.jar:/opt/java/jdk1.8.0_241/jre/lib/ext/nashorn.jar:/opt/java/jdk1.8.0_241/jre/lib/ext/sunec.jar:/opt/java/jdk1.8.0_241/jre/lib/ext/sunjce_provider.jar:/opt/java/jdk1.8.0_241/jre/lib/ext/sunpkcs11.jar:/opt/java/jdk1.8.0_241/jre/lib/ext/zipfs.jar:/opt/java/jdk1.8.0_241/jre/lib/javaws.jar:/opt/java/jdk1.8.0_241/jre/lib/jce.jar:/opt/java/jdk1.8.0_241/jre/lib/jfr.jar:/opt/java/jdk1.8.0_241/jre/lib/jfxswt.jar:/opt/java/jdk1.8.0_241/jre/lib/jsse.jar:/opt/java/jdk1.8.0_241/jre/lib/management-agent.jar:/opt/java/jdk1.8.0_241/jre/lib/plugin.jar:/opt/java/jdk1.8.0_241/jre/lib/resources.jar:/opt/java/jdk1.8.0_241/jre/lib/rt.jar:/root/IdeaProjects/LogUtil/target/classes cn.yzstu.tt.Demo
18
Process finished with exit code 0
然後再看我們的/opt文件夾下有麼有日誌文件,日誌成功寫入
查看日誌文件,註解聲明內容啓用
2020-04-06 00:17:30:記錄Baldwin的年齡::18
5.總結
目前爲止,我們的一個簡單的日誌記錄的輪子已經造好了,我們可以把他打成jar包引入到我們的項目中去,只需要在項目初始化時啓用我們的功能即可。
5.1.思想
思想:給予用戶配置權限
框架是拿給別人用的,一定要給予用戶自主配置的權限
思想:默認配置
很多時候,我們的用戶可能沒有配置需要配置的信息,而且註解中也沒有聲明,那麼就要求我們存在默認的配置來填補這些空缺,從而避免由於空配置導致的錯誤
思想:默認配置
很多時候,我們的用戶可能沒有配置需要配置的信息,而且註解中也沒有聲明,那麼就要求我們存在默認的配置來填補這些空缺,從而避免由於空配置導致的錯誤
思想:聲明大於配置
如果我們在註解中聲明瞭一些用到的信息,但是配置文件中也有這些信息,我們應該有限選用註解中聲明的信息。
思想:自定義掃描路徑
我們應當給予用戶權限去讓他自己規定自己註解使用的包
思想:多態分發
儘量讓我們的隊列能夠處理不同種類的消息,我們在獲取到隊列中的消息之後,應當有一個對消息類型的判斷,並將不同類型的消息分發到不同方法中的操作
5.2.關於本項目
作者是一個正在編程路上匍匐前進的萌新,這篇實例僅提供給新手入門使用,如果有錯誤,還請大佬不吝指點。
項目代碼:https://gitee.com/dikeywork/LogUtil
5.3.個人總結
完成項目時遇到了許多的困難,本來打算一天完事兒,但是真正寫完這篇文章卻用了整整兩天,仍需進步。
我是Baldwin,一個25歲的程序員,致力於讓學習變得更有趣,如果你也真正喜愛編程,真誠的希望與你交個朋友,一起在編程的海洋裏徜徉!
源碼閱讀技巧:https://blog.csdn.net/shouchenchuan5253/article/details/105196154
Java註解詳解:https://blog.csdn.net/shouchenchuan5253/article/details/105145725
教你自建SpringBoot服務器:https://blog.csdn.net/shouchenchuan5253/article/details/104773702
更多文章請點擊:https://blog.csdn.net/shouchenchuan5253/article/details/105020803