由一道Neusoft題中想到的Java日志API
先來(lái)看看這一季度的試題的總體要求:
部門已經(jīng)完成了多次編程考試,為了方便對(duì)每個(gè)人的考試情況進(jìn)行跟蹤,需要
將所有人員的成績(jī)進(jìn)行合并、匯總。
歷次考試成績(jī)格式為Excel格式,共有三列數(shù)據(jù):郵件地址、姓名、成績(jī)。為了
簡(jiǎn)化代碼實(shí)現(xiàn),在統(tǒng)計(jì)時(shí),會(huì)先將Excel格式的成績(jī)單“另存為”保存類型為“文本文件
(制表符分隔)(*.txt)”格式的文件,文件名稱格式為“yyyymm.txt”(即:4位年份2位
月分.txt),作為程序的輸入文件進(jìn)行讀取、合并操作。
輸入文件保存在c:\test\src\文件夾下,此文件夾下不會(huì)有其它文件。在匯總處
理之前,我們會(huì)檢查此文件夾下的輸入文件,確保文件名符合輸入要求。
在讀取文件進(jìn)行處理的過(guò)程中,如果遇到非法的數(shù)據(jù),可以直接跳過(guò)當(dāng)前人員的
成績(jī),繼續(xù)處理其它數(shù)據(jù)。同時(shí),需要將錯(cuò)誤發(fā)生的源文件名,錯(cuò)誤發(fā)生的行數(shù),及
所在行內(nèi)容記錄在日志文件c:\test\test.log文件中。
記錄信息為“數(shù)據(jù)錯(cuò)誤:yyyymm.txt 第 N 行?!?。其中,yyyymm.txt、N分別為實(shí)際
的文件名與行數(shù)。
合并后文件格式仍為文本文件,前兩列為:郵件地址、姓名,從第三列開(kāi)始,按
考試日期先后順序逐一列出每次考試的成績(jī),如果某次考試缺考,則成績(jī)以“--”
代替。合并后文件名稱為“result.txt”,保存在c:\test\文件夾下。
便于后續(xù)做進(jìn)一步檢索與處理,輸出文件格式需要嚴(yán)格符合下面的要求:
1)不需要有表頭列,從文件***行開(kāi)始即為人員的成績(jī)。
2)列寬與對(duì)齊方式:前兩列,“郵件地址”列寬30字符,左對(duì)齊;“姓名”列寬15
字符,左對(duì)齊;從第三列開(kāi)始,列寬統(tǒng)一為4字符且右對(duì)齊。
3)每位人員的成績(jī)?yōu)橐恍袛?shù)據(jù),行末換行要符合windows平臺(tái)習(xí)慣。
4)人員成績(jī)按姓名的漢語(yǔ)拼音順序排序,如果姓名相同,按郵件地址字母順序排序。
附件給出輸入文件與輸出文件的示例,可仔細(xì)閱讀以幫助理解上述格式要求。
提示:1)如果采用Java語(yǔ)言完成,編程過(guò)程中可以使用apache commons包中的api(這個(gè)
建議與考查的內(nèi)容無(wú)關(guān),至少便于對(duì)文件讀寫,評(píng)分是不會(huì)有任何影響)。
例如:固定列寬并且有對(duì)齊要求的文本格式化,可以使用commons-lang包中StringUtils
提供的LeftPad、RightPad方法(當(dāng)然,這現(xiàn)方式并不強(qiáng)制要求,你也可以直接使用jdk
提供的PrintWriter.printf或者String.format或者其它方法這現(xiàn)同樣的目的,選擇自己
熟悉的就可以)
除以上包以外,請(qǐng)使用j2se6.0的標(biāo)準(zhǔn)內(nèi)容。引入其他第3方庫(kù)(如使用數(shù)據(jù)庫(kù))并不符合
考試要求。
2)日志記錄推薦使用log4j或log4net。配置格式不做強(qiáng)制要求,但需要在源文件存在錯(cuò)誤
時(shí)按要求記錄問(wèn)題。
我們?nèi)匀缓雎匀魏蔚谌紸PI,只要是Java API能完成的工作,我們不只用第三方工具。看了這個(gè)需求,其中需要進(jìn)行日志的操作和數(shù)據(jù)格式化輸出,其余就是簡(jiǎn)單IO和一個(gè)合并算法。日中日志我們可以使用java.util.logging的API,數(shù)據(jù)格式化就使用String.format()方法,下面我們來(lái)分析分析。
要使用Java的日志API,結(jié)合使用比較多的Log4j,首先想到的是日志的配置,下面來(lái)看看如何配置Java的日志API:
Java代碼
- package logging;
- import java.text.DateFormat;
- import java.text.SimpleDateFormat;
- import java.util.Date;
- import java.util.logging.Formatter;
- import java.util.logging.LogRecord;
- /**
- * 日志記錄器格式
- *
- * @author Nanlei
- *
- */
- public class LogFormatter extends Formatter {
- @Override
- public String format(LogRecord record) {
- Date date = new Date();
- DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- String dateStr = df.format(date);
- return "[" + dateStr + "] [" + record.getLevel() + "]"
- + record.getClass() + " : " + record.getMessage() + "\n";
- }
- }
這個(gè)類是用來(lái)規(guī)范日志記錄格式的,我們自定義的日志記錄方式可以通過(guò)擴(kuò)展Formatter類來(lái)進(jìn)行,覆蓋其中的format方法即可,其中的程序是生成日期,然后返回我們要在日志中看到的日志格式,這都很好理解,就不多說(shuō)什么了。
寫好了日志格式,那么在實(shí)際中該如何來(lái)使用呢,也很簡(jiǎn)單:
Java代碼
- private static final Logger logger = Logger.getLogger(Main.class.getName());
- private static void setLoggerSettings() throws Exception {
- logger.setLevel(Level.INFO);
- FileHandler fileHandler = new FileHandler("c:\\test\\test.log");
- fileHandler.setFormatter(new LogFormatter());
- logger.addHandler(fileHandler);
- }
在類中聲明一個(gè)靜態(tài)的成員變量,然后對(duì)其進(jìn)行一些設(shè)置,這里包括日志級(jí)別,輸出位置和格式,那么格式就是上面那個(gè)類中設(shè)置的,調(diào)用setLoggerSettings()方法之后,就可以使用日志API了,這很簡(jiǎn)單。
下面來(lái)分析題目需求,要從幾個(gè)文件中來(lái)讀取信息然后進(jìn)行合并,文件中可能有非法數(shù)據(jù),要進(jìn)行處理。文件中的信息包括電子郵件,姓名和每次的成績(jī),要求合并成績(jī)到一條記錄中,那么我們就要首先讀取這些信息,然后進(jìn)行合并處理。信息是用制表符分隔的,那么讀取上來(lái)后就要根據(jù)制表符分割,如果發(fā)現(xiàn)分割出現(xiàn)問(wèn)題,就記錄日志。
首先抽象出數(shù)據(jù)中的對(duì)象,就是考試記錄對(duì)象,我們簡(jiǎn)單刻畫(huà)這個(gè)對(duì)象:
Java代碼
- package bean;
- /**
- * 考試記錄bean
- *
- * @author Nanlei
- *
- */
- public class ExamRecord {
- private String email;// 電子郵件
- private String name;// 人名
- private String record;// 單條成績(jī)
- private String[] records;// 考試記錄
- public ExamRecord() {
- super();
- }
- public ExamRecord(String email, String name, String record) {
- super();
- this.email = email;
- this.name = name;
- this.record = record;
- }
- public String getEmail() {
- return email;
- }
- public void setEmail(String email) {
- this.email = email;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public String getRecord() {
- return record;
- }
- public void setRecord(String record) {
- this.record = record;
- }
- public String[] getRecords() {
- return records;
- }
- public void setRecords(String[] records) {
- this.records = records;
- }
- @Override
- public String toString() {
- return "ExamRecord [email=" + email + ", name=" + name + ", record="
- + record + ", records=" + records + "]";
- }
- }
這里可說(shuō)的不多,主要是構(gòu)造方法,重載的方法有一個(gè)是填充record的,就是每條記錄的成績(jī),而records變量是我們后期進(jìn)行填充的。
我們開(kāi)始編寫readFromFile(String basePath)方法:
Java代碼
- Set
flagSet = new TreeSet(); - List
infoList = new ArrayList(); - List
recordList = new ArrayList(); - List
recordsPerFile = new ArrayList(); // 標(biāo)識(shí)每個(gè)文件中合法記錄的數(shù)量
這些變量用于對(duì)數(shù)據(jù)進(jìn)行處理。flagSet一看名字就是一個(gè)標(biāo)識(shí)位,為什么用Set,因?yàn)楹喜⒅竺咳耸且粭l記錄,那么處理后就合并了,而原始數(shù)據(jù)中一個(gè)人的信息可能有多條,那么我們要記錄到底有多少不重復(fù)的人,就使用Set了,它會(huì)為我們自動(dòng)去除重復(fù)的,同時(shí)TreeSet會(huì)按找字母順序?yàn)槲覀冏詣?dòng)排序,那么需求中的要求就滿足了,我們不用再寫排序的方法。InfoList用于放置從文件連續(xù)讀出的原始數(shù)據(jù),recordList是方法返回的結(jié)果,也是我們寫入結(jié)果文件的最終對(duì)象,recordsPerFile是輔助變量,用于存儲(chǔ)從每個(gè)文件中讀取的文件數(shù)量,這是處理拼裝大規(guī)模數(shù)據(jù)的基本方法,用于后期數(shù)據(jù)處理時(shí)的循環(huán)變量控制。
Java代碼
- File file = new File(basePath);
- if (!file.isDirectory()) {
- logger.info(file.getAbsolutePath() + " is not a directory");
- } else {
- try {
- String[] files = file.list();
- for (int i = 0; i < files.length; i++) {
- File targetFile = new File(basePath + "\\" + files[i]);
- BufferedReader br = new BufferedReader(
- new InputStreamReader(new FileInputStream(
- targetFile), "GBK"));
- String s = null;
- int line = 0;
- int num = 0;
- while ((s = br.readLine()) != null) {
- if (line == 0) {
- } else {
- String[] infos = s.split("\t");
- if (infos.length != 3) {
- logger.info("錯(cuò)誤數(shù)據(jù) " + files[i] + " 第"
- + (line + 1) + "行");
- } else {
- flagSet.add(infos[0] + "\t" + infos[1]);
- infoList.add(new ExamRecord(infos[0], infos[1],
- infos[2]));
- num++;
- }
- }
- line++;
- }
- recordsPerFile.add(num);
- br.close();
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
這部分就是從文件夾下讀取文件并寫入我們準(zhǔn)備的變量中,首先進(jìn)行文件夾判斷,之后開(kāi)始讀取,我們將數(shù)據(jù)分行讀取,然后用split函數(shù)對(duì)原始數(shù)據(jù)進(jìn)行分隔,如果沒(méi)有得到3個(gè)數(shù)據(jù)部分,那么視為該數(shù)據(jù)無(wú)效,就寫日志,如果獲取到了三個(gè)部分,首先將電子郵件和姓名存入Set,剩余信息存入infoList,使用輔助循環(huán)變量num來(lái)計(jì)算數(shù)量。
至此,我們已經(jīng)讀取到所需數(shù)據(jù)了,下面就是對(duì)數(shù)據(jù)進(jìn)行處理了,首先是對(duì)我們的結(jié)果recordList進(jìn)行一些初始化操作:
Java代碼

- Iterator
it = flagSet.iterator(); - String str = null;
- while (it.hasNext()) {
- str = it.next();
- String[] infos = str.split("\t");
- ExamRecord er = new ExamRecord();
- er.setEmail(infos[0]);
- er.setName(infos[1]);
- String[] arrays = new String[recordsPerFile.size()];
- er.setRecords(arrays);
- recordList.add(er);
- }
這里我們可以從flagSet中獲取最終結(jié)果數(shù)量,然后對(duì)應(yīng)寫入recordList并初始化records數(shù)組,就做完了,下面是對(duì)成績(jī)的處理了,這部分涉及到題目的核心算法,下面所示代表我的一個(gè)處理方式,可能不是***的:
Java代碼
- // 開(kāi)始處理成績(jī)
- for (int i = 0; i < recordsPerFile.size(); i++) {
- int num = recordsPerFile.get(i);
- int count = 0;
- while (count < recordList.size()) {
- ExamRecord tmpER = recordList.get(count);
- tmpER.getRecords()[i] = "--";
- for (int j = 0; j < num; j++) {
- ExamRecord er = infoList.get(j);
- if (tmpER.getEmail().equals(er.getEmail())) {
- tmpER.getRecords()[i] = er.getRecord();
- }
- }
- count++;
- }
- for (int k = 0; k < num; k++) {
- infoList.remove(0);
- }
- }
解釋一下:我們首先變量recordsPerFile,這里標(biāo)識(shí)出一共讀取出幾個(gè)文件,每個(gè)文件中的記錄數(shù)量是多少,下面的while循環(huán)是變量recordList,就是我們每次要處理幾個(gè)用戶。進(jìn)入while循環(huán),首先獲取一個(gè)ExamRecord對(duì)象,然后對(duì)其成績(jī)欄位進(jìn)行填充,如果沒(méi)有值,那么就是”--”,下面開(kāi)始處理infoList部分,這里就看到輔助變量的用途了,因?yàn)槊總€(gè)文件中可能沒(méi)有全部人員的記錄,就是文件記錄數(shù)小于總?cè)藬?shù),那么怎么辦呢,每次遍歷多少呢?就是輔助變量中記錄的數(shù)據(jù),兩個(gè)集合的記錄數(shù)不同,也同時(shí)可以遍歷來(lái)進(jìn)行對(duì)比處理,這就是一種方法,如果發(fā)現(xiàn)相同數(shù)據(jù),在相應(yīng)位置填充,***的for循環(huán)是刪除我們處理過(guò)的數(shù)據(jù),每次刪除0號(hào)元素,刪除的次數(shù)是輔助變量中記錄的。此時(shí)我們的數(shù)據(jù)處理完成,獲得了recordList。
下面是寫入文件的操作:
Java代碼
- private static void writeResultToFile(String fileName,
- List
recordList) { - BufferedOutputStream output = null;
- try {
- output = new BufferedOutputStream(new FileOutputStream(fileName));
- for (int i = 0; i < recordList.size(); i++) {
- ExamRecord examRecord = recordList.get(i);
- output.write(String.format("%-30s", examRecord.getEmail())
- .getBytes());
- output.write(String.format("%-15s", examRecord.getName())
- .getBytes());
- for (int j = 0; j < examRecord.getRecords().length; j++) {
- output.write(String.format("%4s",
- examRecord.getRecords()[j]).getBytes());
- }
- output.write("\r\n".getBytes());
- }
- output.flush();
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- if (output != null) {
- output.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
這里就沒(méi)什么多說(shuō)的了,就是需求中要求對(duì)數(shù)據(jù)格式進(jìn)行處理,左對(duì)齊還是右對(duì)齊也很簡(jiǎn)單了。這里使用了C語(yǔ)言中printf()函數(shù)的格式,String的format()方法支持這點(diǎn),就很簡(jiǎn)單了。
主函數(shù)如下:
Java代碼
- public static void main(String[] args) throws Exception {
- long start = System.currentTimeMillis();
- // 設(shè)置Log
- setLoggerSettings();
- // 讀取文件到對(duì)象中
- List
recordList = readFromFile( "c:\\test\\src");- // 將結(jié)果寫入文件
- writeResultToFile("c:\\test\\result.txt", recordList);
- long end = System.currentTimeMillis();
- System.out.println(end - start + " ms!");
- }
執(zhí)行主函數(shù),就可以在相應(yīng)位置看到結(jié)果。

原始數(shù)據(jù)和源碼見(jiàn)原文鏈接:
http://sarin.javaeye.com/blog/941386。
本文系作者本人的探索,希望大家批評(píng)指正。也希望和Neusofter們交流提高。
【編輯推薦】