日志功能在j2ee项目中是一个至关常见的功能,在一个小项目中或许你能够在一个个方法中,使用日志表的Mapper生成一条条的日志记录,但这无非是最烂的作法之一,由于这种作法会让日志Mapper分布到了项目的多处代码中,后续很难管理。而对于大型的项目而言,这种作法根本不能采用。本篇文章将介绍,使用自定义注解,配合AOP,优雅的完成日志功能。java
本文Demo使用的是Spring Boot框架,但并不是只针对Spring Boot,若是你的项目用的是Spring MVC,作下简单的转换便可在你的项目中实现相同的功能。git
在开始编码以前,先介绍下思路:github
在Service层中,涉及到大量业务逻辑操做,咱们每每就须要在一个业务操做完成后(无论成败或失败),生成一条日志,并插入到数据库中。那么咱们能够在这些涉及到业务操做的方法上使用一个自定义注解进行标记,同时将日志记录到注解中。再配合Spring的AOP功能,在监听到该方法执行以后,获取到注解内的日志信息,把这条日志插入到数据便可。web
好了,下面就对上面的理论付出实践。数据库
这里咱们自定义一个日志注解,该注解中的logStr属性将用来保存日志信息。自定义注解代码以下:app
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Inherited @Documented public @interface Log { String logStr() default ""; }
接着就是到Service层中,在须要使用到日志功能的方法上加上该注解。若是业务需求是在添加一个用户以后,记录一条日志,那只须要在添加用户的方法上加上这个自定义注解便可。代码以下:框架
@Service public class UserService { @Log(logStr = "添加一个用户") public Result add(User user) { return ResultUtils.success(); } }
前面的自定义注解只是起到一个标记与存储日志的做用,接下来须要就该使用Spring的AOP功能,拦截方法的执行,经过反射获取到注解及注解中所包含的日志信息。工具
若是你不清楚怎么在Spring Boot中使用AOP功能,建议你去看上一篇文章:SpringBoot(三)之web开发 ,这里再也不赘诉。post
由于代码量不大,就很少废话了,直接贴出日志切面的完整代码,详细状况看代码中的注释:优化
@Component @Aspect public class LogAspect { private Logger logger = LoggerFactory.getLogger(LogAspect.class); // 设置切点表达式 @Pointcut("execution(* com.lqr.service..*(..))") private void pointcut() { } // 方法后置切面 @After(value = "pointcut()") public void After(JoinPoint joinPoint) throws NotFoundException, ClassNotFoundException { // 拿到切点的类名、方法名、方法参数 String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); // 反射加载切点类,遍历类中全部的方法 Class<?> targetClass = Class.forName(className); Method[] methods = targetClass.getMethods(); for (Method method : methods) { // 若是遍历到类中的方法名与切点的方法名一致,而且参数个数也一致,就说明切点找到了 if (method.getName().equalsIgnoreCase(methodName)) { Class<?>[] clazzs = method.getParameterTypes(); if (clazzs.length == args.length) { // 获取到切点上的注解 Log logAnnotation = method.getAnnotation(Log.class); if (logAnnotation != null) { // 获取注解中的日志信息,并输出 String logStr = logAnnotation.logStr(); logger.error("获取日志:" + logStr); // 数据库记录操做... break; } } } } } }
为了验证这种方式是否真的能拿到注解中携带的日志信息,这里建立一个Controller,代码以下:
@RestController public class UserController { @Autowired UserService mUserService; @PostMapping("/add") public Result add(User user) throws NotFoundException { return mUserService.add(user); } }
使用postman访问接口,能够看到注解中的日志信息确实被拿到了。
你觉得这样就结束了吗?不,这仅仅只是实现了日志功能,但称不上优雅,由于存在不方便的地方,下面就说下,如何对这种方式进一步优化,从而作到优雅的处理日志功能。
前面确确实实的使用自定义注解和AOP作到了日志功能,但存在什么问题呢?这个问题不是代码问题,而是业务功能问题。开发中可能有如下几种状况:
简而言之,就是日志内容能够在代码中随意修改。这就有问题了,注解是静态侵入的,要怎么才能作到在代码中动态修改注解中的属性值呢?所幸,javassist能够帮咱们作到这一点,下面就来看看,若是实现该功能。
javassist须要本身导入第三方依赖,若是你项目有使用到Spring Boot的模板功能(thymeleaf),则无须添加依赖。
结合网上查阅到的资料,我对使用javassist动态修改方法上注解及查看注解中属性值的功能作了一个封装,工具类名为:AnnotationUtils(和Spring自带的一个类名字同样,注意不要在代码中导错包了),并使用了单例模式。代码以下:
/** * @描述 注解中属性修改、查看工具 */ public class AnnotationUtils { private static AnnotationUtils mInstance; public AnnotationUtils() { } public static AnnotationUtils get() { if (mInstance == null) { synchronized (AnnotationUtils.class) { if (mInstance == null) { mInstance = new AnnotationUtils(); } } } return mInstance; } /** * 修改注解上的属性值 * * @param className 当前类名 * @param methodName 当前方法名 * @param annoName 方法上的注解名 * @param fieldName 注解中的属性名 * @param fieldValue 注解中的属性值 * @throws NotFoundException */ public void setAnnotatioinFieldValue(String className, String methodName, String annoName, String fieldName, String fieldValue) throws NotFoundException { ClassPool classPool = ClassPool.getDefault(); CtClass ct = classPool.get(className); CtMethod ctMethod = ct.getDeclaredMethod(methodName); MethodInfo methodInfo = ctMethod.getMethodInfo(); ConstPool constPool = methodInfo.getConstPool(); AnnotationsAttribute attr = (AnnotationsAttribute) methodInfo.getAttribute(AnnotationsAttribute.visibleTag); Annotation annotation = attr.getAnnotation(annoName); if (annotation != null) { annotation.addMemberValue(fieldName, new StringMemberValue(fieldValue, constPool)); attr.setAnnotation(annotation); methodInfo.addAttribute(attr); } } /** * 获取注解中的属性值 * * @param className 当前类名 * @param methodName 当前方法名 * @param annoName 方法上的注解名 * @param fieldName 注解中的属性名 * @return * @throws NotFoundException */ public String getAnnotatioinFieldValue(String className, String methodName, String annoName, String fieldName) throws NotFoundException { ClassPool classPool = ClassPool.getDefault(); CtClass ct = classPool.get(className); CtMethod ctMethod = ct.getDeclaredMethod(methodName); MethodInfo methodInfo = ctMethod.getMethodInfo(); AnnotationsAttribute attr = (AnnotationsAttribute) methodInfo.getAttribute(AnnotationsAttribute.visibleTag); String value = ""; if (attr != null) { Annotation an = attr.getAnnotation(annoName); if (an != null) value = ((StringMemberValue) an.getMemberValue(fieldName)).getValue(); } return value; } }
经过上面的工具类(AnnotationUtils)虽然能够实如今代码中动态修改注解中的属性值的功能,但AnnotationUtils方法中须要的参数过多,这里对其作一层封装,不须要在代码中考虑类名、方法名的获取,方便开发。
/** * @描述 日志修改工具 */ public class LogUtils { private static LogUtils mInstance; private LogUtils() { } public static LogUtils get() { if (mInstance == null) { synchronized (LogUtils.class) { if (mInstance == null) { mInstance = new LogUtils(); } } } return mInstance; } public void setLog(String logStr) throws NotFoundException { String className = Thread.currentThread().getStackTrace()[2].getClassName(); String methodName = Thread.currentThread().getStackTrace()[2].getMethodName(); AnnotationUtils.get().setAnnotatioinFieldValue(className, methodName, Log.class.getName(), "logStr", logStr); } }
@Service public class UserService { @Log(logStr = "添加一个用户") public Result add(User user) throws NotFoundException { if (user.getAge() < 18) { LogUtils.get().setLog("添加用户失败,由于用户未成年"); return ResultUtils.error("未成年不能注册"); } if ("男".equalsIgnoreCase(user.getSex())) { LogUtils.get().setLog("添加用户失败,由于用户是个男的"); return ResultUtils.error("男性不能注册"); } LogUtils.get().setLog("添加用户成功,是一个" + user.getAge() + "岁的美少女"); return ResultUtils.success(); } }
使用javassist修改过的注解属性值没法经过java反射正确静态获取,还须要借助javassist来动态获取,因此,LogAspect中的代码修改以下:
@Component @Aspect public class LogAspect { private Logger logger = LoggerFactory.getLogger(LogAspect.class); @Pointcut("execution(* com.lqr.service..*(..))") private void pointcut() { } @After(value = "pointcut()") public void After(JoinPoint joinPoint) throws NotFoundException, ClassNotFoundException { String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); String logStr = AnnotationUtils.get().getAnnotatioinFieldValue(className, methodName, Log.class.getName(), "logStr"); if (!StringUtils.isEmpty(logStr)) { logger.error("获取日志:" + logStr); // 数据库记录操做... } } }
上面代码都编写完了,下面就验证下,是否能够根据业务状况动态注解中的属性值吧。