你還沒找到工作?我已經因爲自己造輪子而提前轉正了

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章