項目需求
**需求1: **web項目一般而言都需要日志記錄,比較常用的是實用log4j來記錄項目的異常日志,將日志單獨存儲于文件當中衍菱,這樣有利于我們快速進行bug 排解涂邀。
**需求2: **異常的記錄一般就是將異常的堆棧保存在文件中,這樣文件大小會急劇上升侍芝,有效異常信息也不能被立即定位研铆,有沒有一種方式可以可以讓我們重寫異常記錄,并保存在異常日志文件當中呢州叠。
**需求3: **在異常日志之上棵红,我們一般還需要對系統(tǒng)中各角色的各個重要操作進行一些日志記錄,以方便我們找到操作人咧栗,以及責任人逆甜。
福利彩蛋
針對1中的需求,我們大家熟悉的是實用log4j進行日志記錄楼熄。配置簡單忆绰,實現快速。
針對2可岂,3中的需求我們一般采用的是基于攔截器實現(Aop思想的一種實現方式)在方法操作之前進行一定的處理错敢,獲取操作人、操作方法名缕粹、操作參數稚茅,異常捕獲與記錄,這樣實現也是完全可以的平斩。
今天記錄的是基于自定義注解和面向切面(AOP)進行統(tǒng)一操作日志以及異常日志記錄的實現亚享。
項目代碼
項目中代碼如下所示:
1.首先定義兩個注解:分別為SystemControllerLog(用于攔截Controller層操作注解,起切點表達式作用绘面,明確切面應該從哪里注入),SystemServiceLog(用于攔截Service層操作注解欺税,起切點表達式作用,明確切面應該從哪里注入)
這兩個注解在切面中定義切點表達式的時候會用到揭璃。
SystemControllerLog.java
package com.fxmms.common.log.logannotation;
import java.lang.annotation.*;
/**
* Created by mark on 16/11/25.
* @usage 自定義注解晚凿,攔截Controller
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemControllerLog {
String description() default "";
}
SystemServiceLog
package com.fxmms.common.log.logannotation;
import java.lang.annotation.*;
/**
* Created by mark on 16/11/25.
* @usage 自定義注解 攔截service
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemServiceLog {
String description() default "";
}
2.接下來定義切面類,這里面主要定義了幾個通知瘦馍,在調用被代理對象目標方法前后歼秽,或目標方法拋出異常之后使用。
package com.fxmms.common.log.logaspect;
import com.fxmms.common.log.logannotation.SystemControllerLog;
import com.fxmms.common.log.logannotation.SystemServiceLog;
import com.fxmms.common.security.ScottSecurityUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
* 切點類
* @author tiangai
* @since 2014-08-05 Pm 20:35
* @version 1.0
*/
@Aspect
@Component
public class SystemLogAspect {
//注入Service用于把日志保存數據庫 nodo service 層實現
//本地異常日志記錄對象
private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);
/**
* Service層切點 使用到了我們定義的 SystemServiceLog 作為切點表達式情组。
* 而且我們可以看出此表達式基于 annotation燥筷。
*
*/
@Pointcut("@annotation(com.fxmms.common.log.logannotation.SystemServiceLog)")
public void serviceAspect() {
}
/**
* Controller層切點 使用到了我們定義的 SystemControllerLog 作為切點表達式箩祥。
* 而且我們可以看出此表達式是基于 annotation 的。
*/
@Pointcut("@annotation(com.fxmms.common.log.logannotation.SystemControllerLog)")
public void controllerAspect() {
}
/**
* 前置通知 用于攔截Controller層記錄用戶的操作
*
* @param joinPoint 連接點
*/
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//請求的IP
String ip = request.getRemoteAddr();
System.out.println(ip+"sdsdsdsdsd");
try {
//控制臺輸出
System.out.println("=====前置通知開始=====");
Object object = joinPoint.getTarget();
System.out.println("請求方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));
System.out.println("方法描述:" + getControllerMethodDescription(joinPoint));
System.out.println("請求人:" + ScottSecurityUtil.getLoginName());
System.out.println("請求IP:" + ip);
//構造數據庫日志對象
//保存數據庫
System.out.println("=====前置通知結束=====");
} catch (Exception e) {
//記錄本地異常日志
logger.error("==前置通知異常==");
logger.error("異常信息:{}", e.getMessage());
}
}
/**
* 異常通知 用于攔截service層記錄異常日志
*
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "serviceAspect()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//獲取請求ip
String ip = request.getRemoteAddr();
//獲取用戶請求方法的參數并組織成字符串
String params = "";
if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
for (int i = 0; i < joinPoint.getArgs().length; i++) {
params += joinPoint.getArgs()[i]+ ",";
}
}
try {
//控制臺輸出
System.out.println("=====異常通知開始=====");
System.out.println("異常代碼:" + e.getClass().getName());
System.out.println("異常信息:" + e.getMessage());
System.out.println("異常方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));
System.out.println("方法描述:" + getServiceMthodDescription(joinPoint));
System.out.println("請求人:" + ScottSecurityUtil.getLoginName());
System.out.println("請求IP:" + ip);
System.out.println("請求參數:" + params);
//構造數據庫日志對象
//保存數據庫
System.out.println("=====異常通知結束=====");
} catch (Exception ex) {
//記錄本地異常日志
logger.error("==異常通知異常==");
logger.error("異常信息:{}", ex);
}
//錄本地異常日志
logger.error("異常方法:{}異常代碼:{}異常信息:{}參數:{}", joinPoint.getTarget().getClass().getName() + joinPoint.getSignature().getName(), e.getClass().getName(), e.getMessage(), params);
}
/**
* 獲取注解中對方法的描述信息 用于service層注解
*
* @param joinPoint 切點
* @return 方法描述
* @throws Exception
*/
public static String getServiceMthodDescription(JoinPoint joinPoint)
throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
String description = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
description = method.getAnnotation(SystemServiceLog.class).description();
break;
}
}
}
return description;
}
/**
* 獲取注解中對方法的描述信息 用于Controller層注解
*
* @param joinPoint 切點
* @return 方法描述
* @throws Exception
*/
public static String getControllerMethodDescription(JoinPoint joinPoint) throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
String description = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
description = method.getAnnotation(SystemControllerLog.class).description();
break;
}
}
}
return description;
}
}
上面的切面類中定義了公共切點 serviceAspect肆氓、serviceAspect袍祖,并實現了Controller層的前置通知,Service業(yè)務邏輯層的異常通知做院。
其中預留了保存日志到數據庫的代碼段盲泛,我們可以根據業(yè)務自行進行填充。
3.創(chuàng)建好切點键耕、切面類之后寺滚,如何讓他起作用呢,我們需要在配置文件中進行配置了屈雄。我將web項目中關于不同層的配置文件進行的切割村视,數據訪問層配置文件是data-access.xml、業(yè)務邏輯層是service-application.xml酒奶、控制層是defalut-servlet.xml
首先看defalut-servlet.xml中的配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--開啟aop-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<mvc:annotation-driven>
<!--json解析-->
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
<context:component-scan base-package="com.fxmms.www.controller">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--掃描日志記錄切面-->
<context:component-scan base-package="com.fxmms.common.log" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
</context:component-scan>
<!--配置異常處理器-->
<context:component-scan base-package="com.fxmms.common.exception_handler" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
</context:component-scan>
<!--因為web.xml中defaultDispatcherServlet對所有請求進行了攔截蚁孔,所以對一些.css .jpg .html .jsp也進行了攔截,所以此配置項
保證對對靜態(tài)資源不攔截-->
<mvc:default-servlet-handler/>
<!--視圖解析器-->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!--配置文件上上傳-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="utf-8"/>
<property name="maxUploadSize" value="10485760000"/>
<property name="maxInMemorySize" value="40960"/>
</bean>
</beans>
注意:以上配置有兩個重要的點:
1.項惋嚎,
2. <aop:aspectj-autoproxy proxy-target-class="true"/>
proxy-target-class="true"默認是false,更改為true時使用的是cglib動態(tài)代理杠氢。這樣只能實現對Controller層的日志記錄。
service-application.xml配置AOP另伍,實現對Service層的日志記錄.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd">
<!--開啟AOP-->
<aop:aspectj-autoproxy/>
<!--設置定時任務-->
<task:annotation-driven/>
<context:component-scan base-package="com.fxmms.www" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>
<!--ioc管理切面-->
<context:component-scan base-package="com.fxmms.common.log" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
</context:component-scan>
<!-- enable the configuration of transactional behavior based on annotations -->
<tx:annotation-driven transaction-manager="txManager"/>
<bean id="txManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
</beans>
這樣Service也是可以實現操作鼻百、異常日志記錄了。
4.在代碼中使用自定義注解摆尝,相當于在目標方法上設置了一個切點温艇,通過切點注入切面。
Controller層上運用SystemControllerLog注解:
TestNullPointExceptionController.java(驗證Controller層中異常堕汞,Controller中調用Service層代碼)
package com.fxmms.www.controller.admin;
import com.fxmms.common.log.logannotation.SystemControllerLog;
import com.fxmms.www.service.TestExceptionLog;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* Created by mark on 16/11/25.
*/
@Controller
public class TestNullPointExceptionController {
private static Log logger = LogFactory.getLog(TestNullPointExceptionController.class);
//自動注入一個Service層類對象
@Autowired
TestExceptionLog testExceptionLog;
@ResponseBody
@RequestMapping("/admin/testexcption")
@SystemControllerLog(description = "testException")//使用 SystemControllerLog注解勺爱,此為切點
public String testException(String str){
return testExceptionLog.equalStr(str);
}
}
** TestExceptionLog.java**
package com.fxmms.www.service;
import com.fxmms.common.log.logannotation.SystemServiceLog;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Service;
/**
* Created by mark on 16/11/25.
*/
@Service
public class TestExceptionLog {
private static Log logger = LogFactory.getLog(TestExceptionLog.class);
@SystemServiceLog(description = "equalstr")
public String equalStr(String str) {
str = null;
if (str.equals("sd")) {
return "sd";
}else {
return "sd";
}
}
}
我在其中手動設置str = null,用于模擬前臺輸入讯检。
程序在運行時會報運行時異常琐鲁。
最終啟動項目項目console日志輸出如下圖所示:
這樣就完成了自定義注解&Aop&自定義異常&操作日志的記錄,而且自定義的注解與切面可以進行重用人灼,操作日志與異常日志可以進行數據庫記錄绣否,后期甚至可以做一個關于異常分析的系統(tǒng),我們可以直接從日志后臺系統(tǒng)中查看異常出現的頻率挡毅,以及定位異常的發(fā)聲位置,明確操作人等暴构。
完跪呈。
福利彩蛋
職位:騰訊OMG 廣告后臺高級開發(fā)工程師;
Base:深圳;
場景:海量數據段磨,To B,To C耗绿,場景極具挑戰(zhàn)性苹支。
基礎要求:
熟悉常用數據結構與算法;
熟悉常用網絡協(xié)議,熟悉網絡編程;
熟悉操作系統(tǒng)误阻,有線上排查問題經驗;
熟悉MySQL,oracle;
熟悉JAVA债蜜,GoLang,c++其中一種語言均可;
可內推究反,歡迎各位優(yōu)秀開發(fā)道友私信[微笑]
期待關注我的開發(fā)小哥哥寻定,小姐姐們私信我,機會很好精耐,平臺對標抖音狼速,廣告生態(tài)平臺,類似Facebook 廣告平臺卦停,希望你們用簡歷砸我~
聯(lián)系方式 微信 13609184526
博客搬家:大坤的個人博客
歡迎評論哦~