Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
文地址: https://www.twilio.com/blog/guide-node-js-logging
原文作者: DOMINIK KUNDEL
翻譯作者: icepy
譯文出處: https://github.com/lightningminers/article/issues/39
當你開始使用 JavaScript 做開發時,你可能學習到的第一件事情就是如何使用 console.log 將內容打印到控制臺。如果你搜索如何調試 JavaScript,你會發現數百個博客文章和 StackOverflow 的文章都指向簡單的 console.log 。因為這是一種常見的做法,我們甚至可以使用 no-console 這樣的規則來確保生產環境不會留下日志。但是,如果我們真的想要記錄這些信息呢?
在這篇博文中,我們將介紹你想要記錄信息的各種情況,Node.js 中的 console.log 和 console.error 之間的區別是什么,以及如何在不使用戶控制臺混亂的情況下在庫中發送日志記錄。
console.log(`Let's go!`);
雖然你可以在瀏覽器和 Node.js 環境中使用 console.log 和 console.error,但在 Node.js 中使用時一定要記住一件重要的事情。
將如下代碼寫入到 index.js 文件中,并在 Node.js 環境里執行:
console.log('Hello there'); console.error('Bye bye');
如圖:
雖然這兩個輸出看起來可能一樣,但系統實際上對它的處理方式有不同。如果你檢查一下 console section of the Node.js documentation 你會發現 console.log 使用 stdout 打印而 console.error 則使用 stderr。
每一個進程都有三個可以使用的默認 streams,它們是 stdin ,stdout 和 stderr。 stdin 可以處理進程的輸入,例如按下按鈕或重定向輸出。stdout 可以用于處理進程的輸出。最后 stderr 則用于錯誤消息。如果你想了解 stderr 為什么存在以及何時使用它,可以訪問:When to use STDERR instead of STDOUT。
簡而言之,這允許我們使用重定向 > 和管道 | 運算符來處理與應用程序的實際結果分開的錯誤和診斷信息。而 > 允許我們將命令的輸出重定向到文件,2> 允許我們將 stderr 的輸出重定向到文件。我們來看一個例子,它會將 Hello there 重定向輸出到 hello.log ,Bye bye 重定向輸出到 error.log。:
$ node index.js > hello.log 2> error.log
如圖:
現在我們已經了解了日志記錄的基礎技術,那么讓我們來談談你可能想要記錄某些內容的不同例子,通常這些例子都屬于以下類別之一:
我們將跳過本博文中的前兩篇文章,并將重點介紹基于Node.js的三篇文章。
Your Server Application Logs
你希望在服務器上記錄內容的原因可能有多種,例如:記錄傳入的請求,統計信息,有多少404用戶正在訪問,另外你也想知道什么時候出錯以及為什么。
初始化項目:
$ npm init -y $ npm install express
讓我們設置一個帶有中間件的服務器,只需要 console.log 為你的請求提供打印:
const express=require("express"); const PORT=process.env.PORT || 3000; const app=express(); app.use((req,res,next)=> { console.log('%o', req); next(); }); app.get('/', (req,res)=> { res.send('hello world'); }); app.listen(PORT, ()=> { console.log('Server running on port %d', PORT); });
這里我們使用 console.log('%o', req); 來記錄整個對象。
當你運行 node index.js 并訪問 http://localhost:3000 你會注意到打印的很多信息并不是我們需要的。
如果將起更改為 console.log('%s',req) 我們也不會獲取太多的信息。
我們可以編寫自己的日志功能,只打印我們關心的信息。但讓我們先退一步,談談我們通常關心的事情。雖然這些信息經常成為我們關注的焦點,但實際上我們可能需要其他信息,例如:
另外,既然我們知道打印最后都會落到 stdout 和 stderr 上,那么我們可能想要不同日志級別的記錄以及過濾它的能力。
我們可以通過訪問流程的各個部分并編寫一堆 JavaScript 代碼來獲取上述的信息,但 npm 生態已經給我們提供了各種各樣的庫來使用,例如:
我個人喜歡 pino,因為它速度快,生態全。那么,讓我們來看一看 pino 是如何幫助我們記錄日志的。
$ npm install pino express-pino-logger const express=require("express"); const pino=require("pino"); const expressPino=require("express-pino-logger"); const logger=pino({ level: process.env.LOG_LEVEL || 'info'}); const expressLogger=expressPino({ logger }); const PORT=process.env.PORT || 3000; const app=express(); app.use(expressLogger); app.get('/', (req,res)=> { logger.debug('Calling res.send') res.send('hello world'); }); app.listen(PORT, ()=> { logger.info('Server running on port %d', PORT); });
運行 node index.js 并訪問 http://localhost:3000 你可以看到一行一行的 JSON 輸出:
如果你檢查此 JSON ,你會看到前面提到的時間戳。你可能還注意到了我們 logger.debug 語句并未打印,那是因為我們必須更改默認日志級別才能使其可見,試試 LOG_LEVEL=debug node index.js 來調整日志級別。
在此之前我們還需要解決一下日志信息的可讀性,pino 遵循了一個理念,就是為了性能,你應該通過管道將輸出的處理移動到單獨的進程中,你可以去查看一下文檔,了解其中 pino 的錯誤為什么不會寫入 stderr。
讓我們使用 pino-pretty 工具來查看更易讀的日志:
$ npm install --save-dev pino-pretty $ LOG_LEVEL=debug node index.js | ./node_modules/.bin/pino-pretty
運行 LOG_LEVEL=debug node index.js | ./node_modules/.bin/pino-pretty 并訪問 http://localhost:3000。
如圖:
另外還有各種各樣的庫可以來美化你的日志,甚至你可以使用 pino-colada 用 emojis 來顯示它們。這些對于你的本地開發非常有用,在運行到生產服務器之后,你可能希望將日志的管道轉移到另外一個管道,使用 > 將它們寫入硬盤以便稍后處理它們。
比如:
$ LOG_LEVEL=debug node index.js | ./node_modules/.bin/pino-pretty | > success.log 2> s_error.log
既然我們研究了如何有效的為服務器應用程序編寫日志,那么為什么不能將它用在我們的某些庫中呢?問題是,你的庫可能希望記錄用于調試的內容,但實際上不應該讓使用者的應用程序變得混亂。相反,如果需要調試某些東西,使用者應該能夠啟動日志。你的庫默認情況下不會處理這些,并將輸入輸出的操作留給使用者。
express 就是一個很好的例子。
在 express 框架下有很多事情要做,在調試應用程序時,你可能希望了解一下框架的內容。如果我們查詢文檔,你會注意到你可以在命令行的前面加上 DEBUG=express:* 來啟動。
$ DEBUG=express:* node index.js
如圖:
如果你沒有啟動調試日志,則不會看到任何這樣的日志輸出。這是通過一個叫 debug 的包來完成的。
$ npm install debug
讓我們創建一個新的文件 random-id.js 來使用它:
const debug=require("debug"); const log=debug("mylib:randomid"); log("Library loaded"); function getRandomId() { log('Computing random ID'); const outcome=Math.random() .toString(36) .substr(2); log('Random ID is "%s"', outcome); return outcome; } module.exports={ getRandomId };
這里會創建一個帶有命名空間為 mylib:randomid 的 debug 記錄器,然后會將這兩種消息記錄上去。
我們可以在 index.js 文件中引用它:
const express=require("express"); const pino=require("pino"); const expressPino=require("express-pino-logger"); const randomId=require("./random-id"); const logger=pino({ level: process.env.LOG_LEVEL || 'info'}); const expressLogger=expressPino({ logger }); const PORT=process.env.PORT || 3000; const app=express(); app.use(expressLogger); app.get('/', (req,res)=> { logger.debug('Calling res.send') const id=randomId.getRandomId(); res.send(`hello world [${id}]`); }); app.listen(PORT, ()=> { logger.info('Server running on port %d', PORT); });
然后使用 DEBUG=mylib:randomid node index.js 來重新運行你的 index.js 文件,如圖:
有意思的是,如果你的庫使用者想把這些調試信息集成到自己的 pino 日志中去,那么他們可以使用一個叫 pino-debug 的庫來正確的格式化這些日志。
$ npm install pino-debug
pino-debug 在我們第一次使用之前需要初始化一次 debug,最簡單的方法就是在啟動之前使用 Node.js 的 -r 或 --require 命令來初始化。
$ DEBUG=mylib:randomid node -r pino-debug index.js | ./node_modules/.bin/pino-colada
如圖:
我將在這篇博文中介紹最后一個案例,針對 CLI 的日志記錄。我的理念是將邏輯日志和你的 CLI 輸出分開。對于任何邏輯日志來說,你應該使用類似 debug 這樣的包。這樣你或其他人就可以重寫邏輯,而不受 CLI 的約束。
一種情況是你的 CLI 在持續集成的系統中被使用,因此你可能希望刪除各種花里胡哨的輸出。有些 CI 系統設置了一個被稱為 CI 的環境標志。如果你想更安全的檢查自己是否在 CI 系統中,你可以使用 is-ci 這個庫。
有些庫例如 chalk 已經為你檢查了 CI 并幫你刪除了顏色。
$ npm install chalk const chalk=require("chalk"); console.log('%s Hi there', chalk.cyan('INFO'));
運行 node cli.js,如圖:
當你運行 CI=true node cli.js,如圖:
你要記住的是另外一個場景 stdout 能否在終端模式中運行。如果是這種情況,我們可以使用類似 boxen的東西來顯示所有漂流的輸出。但如果不是,則可能會將輸出重定向到文件或輸出到其他地方。
你可以使用 isTTY 來檢查 stdout,stdin,stderr 是否在終端模式。
如:
process.stdout.isTTY
根據 Node.js 的啟動方式,這個三個的值可能不同。你可以在文檔中找到更多關于它的信息。
讓我們看看 process.stdout.isTTY 在不同情況下的變化:
const chalk=require("chalk"); console.log(process.stdout.isTTY); console.log('%s Hi there', chalk.cyan('INFO'));
然后運行 node index.js ,如圖:
之后運行相同的內容,但將其輸出重定向到一個文件中,這次你會看見它會打印一個 undefined 后面跟著一個簡單的無色消息。
這是因為 stdout 關閉了終端模式下 stdout 的重定向。
chalk 使用了 supports-color ,它會在引擎里檢查各個流的 isTTY。
像 chalk 這樣的庫已經幫你處理了這些行為,但在開發 CLI 的過程中還是要注意,在 CI 模式下運行或輸出被重定向的問題。
例如,你可以在終端以一種漂亮的方式來排列數據,如果 isTTY 為 undefined 時,則切換到更容易解析的方式上。
在 JavaScript 中使用 console.log 是非常快的,但當你將代碼部署到生產環境時,你應該要考慮更多關于記錄的內容。
本文僅僅是介紹了各種方法和可用的日志記錄解決方案,它不包含你需要知道的一切。
因此我建議你多看一看你喜歡的開源項目,看看它們是如何解決日志記錄問題以及它們所使用的工具。
一節我們完成了基本的設置,應該說,至少目前對我來說,現在的功能已經基本夠用了。
于是我計劃從這篇文章開始,我就不再使用Word進行編輯了,用我這個私人日記軟件來編輯,實際驗證下是否實用。
如下圖,這是我之前教程的編寫模式:
之前也交待過,在word文檔中編輯,只要是包含了圖片的,復制后再粘貼到頭條都沒辦法成功,還需要把圖片一個個另外粘貼過去才行。如果是遇到gif動圖,從word復制只能復制靜態的截圖,還得再次復制原文件才能粘貼到頭條的編輯器中,操作起來比較麻煩。
于是我就在我的程序中嘗試復制粘貼,發現跟word是一樣的效果。這對我來說是不實用的,需要對它進行改造。
要解決這個問題,首先要分析下為什么會造成這種情況。我們使用的文本編輯控件是RichTextBox,這個默認是rtf格式的,跟word是一樣的。我們把帶圖文的內容粘貼到word文檔中,圖片就是可以正常顯示的。這就說明我們Ctrl+C的內容是rtf格式的,而不是通過瀏覽器粘貼能夠識別出來的格式。那么瀏覽器能夠識別的格式是不是每個瀏覽器特有的呢?于是我嘗試在FireFox中復制圖片和文字,粘貼到用Chrome打開的頭條編輯器,發現圖片是可以顯示的,上述的猜測可以排除了。那么這個格式究竟是怎樣的呢?我需要在程序中截獲一下粘貼的內容。在ContentManagerForm.cs文件中,添加鍵盤截獲事件,截獲Ctrl+V時的內容:
我們發現,如果從瀏覽器上按Ctrl+C,到我們的軟件中按Ctrl+V,用DataFormats.Html做參數可以取到值,值的內容大概是這樣:
前面紅線部分相當于信息頭,定義了一些參數,后面從<div>開始就是我們剛剛復制的內容了。我原封不動地將這些內容再寫回到剪貼板中,然后在瀏覽器中按Ctrl+V,瀏覽器也能夠正常顯示了。
這樣我們基本上就明白了,想要跟瀏覽器正常通訊,需要兩步:第一步是要將rtf格式轉換為html格式,第二步是要將html再轉換成瀏覽器能夠識別的格式到剪貼板。
一般這種格式轉換需要的工作量都比較大,自己從頭開發不大現實,需要網上找找看是否有現成的類庫。對比了幾家,找到了一個rftpipe的,而且是mit授權的。于是從Nuget獲取安裝,使用起來很容易:
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
string html=RtfPipe.Rtf.ToHtml(tbxMainBody.Rtf);
如此兩行代碼就實現了第一步的轉換。
同樣也是先網上查資料,找到了一個ClipboardHelper的類,同樣也是mit授權的。把代碼加入到工程,然后在ContentManagerForm中增加一個專門用于復制成html格式到剪貼板的按鈕:
按鈕處理代碼:
private void btnCopyAsHtml_Click(object sender, EventArgs e)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
string html=RtfPipe.Rtf.ToHtml(tbxMainBody.Rtf);
ClipboardHelper.CopyToClipboard(html, "");
}
在使用了類庫的基礎上,三行代碼就完成了我想要的功能。
功能演示:
以上解決了從軟件復制到頭條的問題。
還有一種場景,就是把瀏覽器當成來源,從瀏覽器復制到軟件的功能。如果是純文本RichEdit編輯器自帶就支持,如果是圖文混排的,就需要從html到rtf的轉換,我找了一下相關的類庫,貌似沒有類似rftpipe這種免費且直接支持字符轉換的庫,很多都是通過文檔來轉換,而且還是收費的。好不容易找到一個符合要求的,結果這個項目比較老,是在Framework下開發的,不支持.netcore。
鑒于找到合適的庫有點困難,隨后仔細分析了下需求,其實這種從瀏覽器圖文混排復制到軟件的場景對我來說意義不大,通篇復制是不大可能的,關鍵位置復制文字做摘抄,真的有特別需要的圖片,單張截圖就好,所以這個功能暫時擱置吧。
這個小項目伴著教程做到了24節,終于有了一點實際作用了:)
----------------------------------------------------
本教程盡量保證2天一更,項目源碼已作為開源項目加入到Gitee,代碼內容會隨教程實時更新,大家有興趣的話可以關注我,以獲得最及時的更新。私信:
私人日記 可以獲取Gitee的鏈接;
sqlitestudio 可以獲取sqlitestudio的鏈接;
菜鳥 可以獲取菜鳥教程鏈接;
QQ群 可以獲取教程交流Q群號;
大家閱讀過程中有哪些看不懂或未盡興的地方,可以在評論區留言,我會先記下來在后續的教程中找機會再說。
教程有幫助的話請大家幫忙關注、轉發、擴散,能不能開專欄還需要你們的支持!
前看到網上很多是基于切面類Aspect去實現了,在切面類中定義before after around等邏輯以及要攔截等方法。本文利用注解實現了一套可以擴展等日志記錄模塊。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public abstract @interface RequiredLogInterceptor { boolean required() default true; String targetGenerator() default ""; OperateType operateType() default OperateType.GET; }
requried:注解是否生效
targetGenerator: 每個模塊記錄等內容不同,入口參數不同,所以需要個性化定制日志等記錄內容,每個模塊的日志生成有自己定義的generator類,并且重寫generateContent方法。
operateType:當前方法是增加,刪除,還是修改
public abstract class ContentGerator { public static String SPLIT="/"; public static String CONTENT_SPLIT=","; public static String VALUE_SPLIT=":"; abstract List<UserOperateLog> generateContent(Object returnValue, Object[] args, OperateType operateType); }
本模塊主要是后置通知,主要邏輯如下:
1.攔截方法,判斷是否有注解loginterceptor
2. 如果有判斷是否執行成功,成功則記錄log,失敗不記錄
3. 獲取注解中配置的generator類,利用反射調用generateContent方法,生成個性化日志內容
5.在日志中添加其他公共屬性,比如用戶id,創建時間等等。所有個性化定制的日志信息都是在generator類中產生。
public class LogAfterInterceptor implements AfterReturningAdvice { @Autowired private LogService logService; @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { RequiredLogInterceptor requiredLogInterceptor=AnnotationUtils.findAnnotation(method, RequiredLogInterceptor.class); if (requiredLogInterceptor !=null) { if(returnValue!=null&&returnValue instanceof Response){ Response response=(Response)returnValue; String code=response.getCode(); String code200=MegCodeEnums.ResponseCodeEnum.C200.getCode(); String code201=MegCodeEnums.ResponseCodeEnum.C201.getCode(); if (!Strings.isNullOrEmpty(code)&&!code.equalsIgnoreCase(code200)&&!code.equalsIgnoreCase(code201)){ return; } } String targetGeneratorName=requiredLogInterceptor.targetGenerator(); OperateType operateType=requiredLogInterceptor.operateType(); Class targetGeneratorclass=Class.forName("com.puhui.flowplatform.manage.log."+targetGeneratorName); Method executeMethod=targetGeneratorclass.getMethod("generateContent",Object.class,Object[].class,OperateType.class); ContentGerator targetGeneratorBean=(ContentGerator)SpringContextHolder.getBean(targetGeneratorclass); List<UserOperateLog> userOperateLogList=(List<UserOperateLog>)executeMethod.invoke(targetGeneratorBean,returnValue,args,operateType); if(CollectionUtils.isNotEmpty(userOperateLogList)){ userOperateLogList.forEach(userOperateLog -> { userOperateLog.setCreateTime(new Date()); //token long userId=0L; if (args.length>0&&args[0] instanceof String){ userId=CommonUtils.getManageCurUserId(args[0].toString()); } userOperateLog.setUserId(userId); }); logService.batchInsertLog(userOperateLogList); } } } }
繼承統一的ContentGenerator類,便于共享一些常量。根據當前操作類型,生成對應的日志內容就可以了。如果需要新增模塊, 先定義自己的日志generator類,然后添加注解到對應模塊就可以。
@Service public class ContentGeneratorForRoleMgt extends ContentGerator { @Autowired private MenuService menuService; private String generateMenus(VoRole voRole){ List<Menus> menusList=voRole.getMenusList(); StringBuffer stringBuffer=new StringBuffer(); if (CollectionUtils.isNotEmpty(menusList)){ menusList.forEach(menus -> { Long menuId=menus.getId(); Menus menusTemp=menuService.queryMenuByMenuId(menuId); stringBuffer.append(menusTemp.getDisplayTitle()+CONTENT_SPLIT); }); stringBuffer.deleteCharAt(stringBuffer.length() - 1); } return stringBuffer.toString(); } @Override public List<UserOperateLog> generateContent(Object returnValue, Object[] args, OperateType operateType) { { List<UserOperateLog> userOperateLogList=new ArrayList<>(); UserOperateLog userOperateLog=new UserOperateLog(); if (operateType==OperateType.ADD||operateType==OperateType.UPDATE){ VoRole voRole=(VoRole)args[1]; String menus=generateMenus(voRole); userOperateLog.setOperateContent("角色名稱"+VALUE_SPLIT+voRole.getDisplayName()+SPLIT+"權限"+VALUE_SPLIT+menus); userOperateLog.setOperateType(operateType==OperateType.ADD?LogOperateTypeEnum.ADD_ROLE.getCode():LogOperateTypeEnum.UPDATE_ROLE.getCode()); } if (operateType==OperateType.DELETE){ if(returnValue!=null){ Response response=(Response) returnValue; String roleName=response.getData().toString(); userOperateLog.setOperateContent(roleName); userOperateLog.setOperateType(LogOperateTypeEnum.DELETE_ROLE.getCode()); } } userOperateLogList.add(userOperateLog); return userOperateLogList; } } }
@PutMapping(value="roles/{roleId}") @RequiredLogInterceptor(targetGenerator="ContentGeneratorForRoleMgt",operateType=OperateType.UPDATE) @ApiOperation(value="修改角色", httpMethod="PUT", response=Response.class, notes="修改角色") public Response<Object> updateRole(@RequestHeader String token,@RequestBody VoRole voRole, @PathVariable("roleId") String roleId) { LOGGER.info("updateRole入參:{}", JSONObject.toJSONString(voRole));
public class SpringMvcConfig extends WebMvcConfigurerAdapter { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { super.configureMessageConverters(converters); // 初始化轉換器 FastJsonHttpMessageConverter fastConvert=new FastJsonHttpMessageConverter(); // 初始化一個轉換器配置 FastJsonConfig fastJsonConfig=new FastJsonConfig(); fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); // 將配置設置給轉換器并添加到HttpMessageConverter轉換器列表中 fastConvert.setFastJsonConfig(fastJsonConfig); converters.add(fastConvert); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/swagger-ui.html").addResourceLocations( ResourceUtils.CLASSPATH_URL_PREFIX + "/META-INF/resources/"); registry.addResourceHandler("/static/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX + "/static/", ResourceUtils.CLASSPATH_URL_PREFIX + "/dist/static/"); registry.addResourceHandler("/page/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX + "/dist/"); super.addResourceHandlers(registry); } @Bean public ViewResolver viewResolver() { FreeMarkerViewResolver resolver=new FreeMarkerViewResolver(); resolver.setCache(true); resolver.setPrefix(ResourceUtils.CLASSPATH_URL_PREFIX + "templates/"); resolver.setSuffix(".ftl"); resolver.setContentType("text/html; charset=UTF-8"); return resolver; } // 創建Advice或Advisor @Bean public BeforeAdvice beforeControllerInterceptor() { return new BeforeControllerInterceptor(); } @Bean public AfterAdvice logAfterInterceptor() { return new LogAfterInterceptor(); } // 創建Advice或Advisor @Bean public BeforeAdvice logBeforeInterceptor() { return new LogBeforeInterceptor(); } // 使用BeanNameAutoProxyCreator來創建代理 @Bean public BeanNameAutoProxyCreator beanAutoProxyCreator() { BeanNameAutoProxyCreator beanNameAutoProxyCreator=new BeanNameAutoProxyCreator(); beanNameAutoProxyCreator.setProxyTargetClass(true); // 設置要創建代理的那些Bean的名字 beanNameAutoProxyCreator.setBeanNames("*Controller"); // // 設置攔截鏈名字(這些攔截器是有先后順序的) beanNameAutoProxyCreator.setInterceptorNames("logAfterInterceptor"); return beanNameAutoProxyCreator; } @Bean public BeanNameAutoProxyCreator beanBeforeAutoProxyCreator() { BeanNameAutoProxyCreator beanNameAutoProxyCreator=new BeanNameAutoProxyCreator(); beanNameAutoProxyCreator.setProxyTargetClass(true); // 設置要創建代理的那些Bean的名字 beanNameAutoProxyCreator.setBeanNames("*Controller"); // 設置攔截鏈名字(這些攔截器是有先后順序的) beanNameAutoProxyCreator.setInterceptorNames("beforeControllerInterceptor"); beanNameAutoProxyCreator.setInterceptorNames("logBeforeInterceptor");
本來實現都代碼版本中,所有都日志生成代碼都在后置攔截器中,并且根據當前執行都方法都classname和methodname去判斷當前都方法,出現很多if 判斷,且method name都不一樣,有的是addXXX,有的是createXXX,顯然設計不合理。后來重新進行了設計,有什么不足,希望大家可以指出。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。