Spring AOP + 注解实现统一注解功能
1.概述
在一般系统中,当我们做了一些重要的操作时,如登陆系统,添加用户,删除用户等操作时,我们需要将这些行为持久化。本文我们通过SpringAOP和Java的自定义注解来实现日志的插入。此方案对原有业务入侵较低,实现较灵活
2.日志的相关类定义
我们将日志抽象为以下两个类:功能模块和操作类型
使用枚举类定义功能模块类型ModuleType,如学生、用户模块
publicenumModuleType{
DEFAULT("1"),//默认值
STUDENT("2"),//学生模块
TEACHER("3");//用户模块
privateModuleType(Stringindex){
this.module=index;
}
privateStringmodule;
publicStringgetModule(){
returnthis.module;
}
}
使用枚举类定义操作的类型:EventType。如登陆、添加、删除、更新、删除等
publicenumEventType{
DEFAULT("1","default"),ADD("2","add"),UPDATE("3","update"),DELETE_SINGLE("4","delete-single"),
LOGIN("10","login"),LOGIN_OUT("11","login_out");
privateEventType(Stringindex,Stringname){
this.name=name;
this.event=index;
}
privateStringevent;
privateStringname;
publicStringgetEvent(){
returnthis.event;
}
publicStringgetName(){
returnname;
}
}
3.定义日志相关的注解
3.1.@LogEnable
这里我们定义日志的开关量,类上只有这个值为true,这个类中日志功能才开启
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public@interfaceLogEnable{
/**
*如果为true,则类下面的LogEvent启作用,否则忽略
*@return
*/
booleanlogEnable()defaulttrue;
}
3.2.@LogEvent
这里定义日志的详细内容。如果此注解注解在类上,则这个参数做为类全部方法的默认值。如果注解在方法上,则只对这个方法启作用
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.METHOD,ElementType.TYPE})
public@interfaceLogEvent{
ModuleTypemodule()defaultModuleType.DEFAULT;//日志所属的模块
EventTypeevent()defaultEventType.DEFAULT;//日志事件类型
Stringdesc()default"";//描述信息
}
3.3.@LogKey
此注解如果注解在方法上,则整个方法的参数以json的格式保存到日志中。如果此注解同时注解在方法和类上,则方法上的注解会覆盖类上的值。
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public@interfaceLogKey{
StringkeyName()default"";//key的名称
booleanisUserId()defaultfalse;//此字段是否是本次操作的userId,这里略
booleanisLog()defaulttrue;//是否加入到日志中
}
4.定义日志处理类
4.1.LogAdmModel
定义保存日志信息的类
publicclassLogAdmModel{
privateLongid;
privateStringuserId;//操作用户
privateStringuserName;
privateStringadmModel;//模块
privateStringadmEvent;//操作
privateDatecreateDate;//操作内容
privateStringadmOptContent;//操作内容
privateStringdesc;//备注
set/get略
}
4.2.ILogManager
定义日志处理的接口类ILogManager
我们可以将日志存入数据库,也可以将日志发送到开中间件,如果redis,mq等等。每一种日志处理类都是此接口的实现类
publicinterfaceILogManager{
/**
*日志处理模块
*@paramparamLogAdmBean
*/
voiddealLog(LogAdmModelparamLogAdmBean);
}
4.3.DBLogManager
ILogManager实现类,将日志入库。这里只模拟入库
@Service
publicclassDBLogManagerimplementsILogManager{
@Override
publicvoiddealLog(LogAdmModelparamLogAdmBean){
System.out.println("将日志存入数据库,日志内容如下:"+JSON.toJSONString(paramLogAdmBean));
}
}
5.AOP的配置
5.1.LogAspect定义AOP类
使用@Aspect注解此类
使用@Pointcut定义要拦截的包及类方法
我们使用@Around定义方法
@Component
@Aspect
publicclassLogAspect{
@Autowired
privateLogInfoGenerationlogInfoGeneration;
@Autowired
privateILogManagerlogManager;
@Pointcut("execution(*com.hry.spring.mvc.aop.log.service..*.*(..))")
publicvoidmanagerLogPoint(){
}
@Around("managerLogPoint()")
publicObjectaroundManagerLogPoint(ProceedingJoinPointjp)throwsThrowable{
….
}
}
aroundManagerLogPoint:主方法的主要业务流程
1.检查拦截方法的类是否被@LogEnable注解,如果是,则走日志逻辑,否则执行正常的逻辑
2.检查拦截方法是否被@LogEvent,如果是,则走日志逻辑,否则执行正常的逻辑
3.根据获取方法上获取@LogEvent中值,生成日志的部分参数。其中定义在类上@LogEvent的值做为默认值
4.调用logInfoGeneration的processingManagerLogMessage填充日志中其它的参数,做个方法我们后面再讲
5.执行正常的业务调用
6.如果执行成功,则logManager执行日志的处理(我们这里只记录执行成功的日志,你也可以定义记录失败的日志)
@Around("managerLogPoint()")
publicObjectaroundManagerLogPoint(ProceedingJoinPointjp)throwsThrowable{
Classtarget=jp.getTarget().getClass();
//获取LogEnable
LogEnablelogEnable=(LogEnable)target.getAnnotation(LogEnable.class);
if(logEnable==null||!logEnable.logEnable()){
returnjp.proceed();
}
//获取类上的LogEvent做为默认值
LogEventlogEventClass=(LogEvent)target.getAnnotation(LogEvent.class);
Methodmethod=getInvokedMethod(jp);
if(method==null){
returnjp.proceed();
}
//获取方法上的LogEvent
LogEventlogEventMethod=method.getAnnotation(LogEvent.class);
if(logEventMethod==null){
returnjp.proceed();
}
StringoptEvent=logEventMethod.event().getEvent();
StringoptModel=logEventMethod.module().getModule();
Stringdesc=logEventMethod.desc();
if(logEventClass!=null){
//如果方法上的值为默认值,则使用全局的值进行替换
optEvent=optEvent.equals(EventType.DEFAULT)?logEventClass.event().getEvent():optEvent;
optModel=optModel.equals(ModuleType.DEFAULT)?logEventClass.module().getModule():optModel;
}
LogAdmModellogBean=newLogAdmModel();
logBean.setAdmModel(optModel);
logBean.setAdmEvent(optEvent);
logBean.setDesc(desc);
logBean.setCreateDate(newDate());
logInfoGeneration.processingManagerLogMessage(jp,
logBean,method);
ObjectreturnObj=jp.proceed();
if(optEvent.equals(EventType.LOGIN)){
//TODO如果是登录,还需要根据返回值进行判断是不是成功了,如果成功了,则执行添加日志。这里判断比较简单
if(returnObj!=null){
this.logManager.dealLog(logBean);
}
}else{
this.logManager.dealLog(logBean);
}
returnreturnObj;
}
/**
*获取请求方法
*
*@paramjp
*@return
*/
publicMethodgetInvokedMethod(JoinPointjp){
//调用方法的参数
ListclassList=newArrayList();
for(Objectobj:jp.getArgs()){
classList.add(obj.getClass());
}
Class[]argsCls=(Class[])classList.toArray(newClass[0]);
//被调用方法名称
StringmethodName=jp.getSignature().getName();
Methodmethod=null;
try{
method=jp.getTarget().getClass().getMethod(methodName,argsCls);
}catch(NoSuchMethodExceptione){
e.printStackTrace();
}
returnmethod;
}
}
6.将以上的方案在实际中应用的方案
这里我们模拟学生操作的业务,并使用上文注解应用到上面并拦截日志
6.1.IStudentService
业务接口类,执行一般的CRUD
publicinterfaceIStudentService{
voiddeleteById(Stringid,Stringa);
intsave(StudentModelstudentModel);
voidupdate(StudentModelstudentModel);
voidqueryById(Stringid);
}
6.2.StudentServiceImpl:
@LogEnable:启动日志拦截
类上@LogEvent定义所有的模块
方法上@LogEven定义日志的其它的信息
@Service
@LogEnable//启动日志拦截
@LogEvent(module=ModuleType.STUDENT)
publicclassStudentServiceImplimplementsIStudentService{
@Override
@LogEvent(event=EventType.DELETE_SINGLE,desc="删除记录")//添加日志标识
publicvoiddeleteById(@LogKey(keyName="id")Stringid,Stringa){
System.out.printf(this.getClass()+"deleteByIdid="+id);
}
@Override
@LogEvent(event=EventType.ADD,desc="保存记录")//添加日志标识
publicintsave(StudentModelstudentModel){
System.out.printf(this.getClass()+"savesave="+JSON.toJSONString(studentModel));
return1;
}
@Override
@LogEvent(event=EventType.UPDATE,desc="更新记录")//添加日志标识
publicvoidupdate(StudentModelstudentModel){
System.out.printf(this.getClass()+"saveupdate="+JSON.toJSONString(studentModel));
}
//没有日志标识
@Override
publicvoidqueryById(Stringid){
System.out.printf(this.getClass()+"queryByIdid="+id);
}
}
执行测试类,打印如下信息,说明我们日志注解配置启作用了:
将日志存入数据库,日志内容如下:
{"admEvent":"4","admModel":"1","admOptContent":"{\"id\":\"1\"}","createDate":1525779738111,"desc":"删除记录"}
7.代码
以上的详细的代码见下面
github代码,请尽量使用tagv0.21,不要使用master,因为我不能保证master代码一直不变