aopaspectjunity-interception

Customized parameter logging when using aspect oriented programing


All the examples I've seen that use aspect oriented programming for logging either log just class, method name and duration, and if they log parameters and return values they simply use ToString(). I need to have more control over what is logged. For example I want to skip passwords, or in some cases log all properties of an object but in other cases just the id property. Any suggestions? I looked at AspectJ in Java and Unity interception in C# and could not find a solution.


Solution

  • You could try introducing parameter annotations to augment your parameters with some attributes. One of those attributes could signal to skip logging the parameter, another one could be used to specify a converter class for the string representation.

    With the following annotations:

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Log {
    }
    
    
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface SkipLogging {
    }
    
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface ToStringWith {
        Class<? extends Function<?, String>> value();
    }
    

    the aspect could look like this:

    import java.lang.reflect.Parameter;
    import java.util.function.Function;
    import java.util.stream.Collectors;
    import java.util.stream.IntStream;
    
    import org.aspectj.lang.reflect.MethodSignature;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public aspect LoggingAspect {
    
        private final static Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
    
        pointcut loggableMethod(): execution(@Log * *..*.*(..));
    
        before(): loggableMethod() {
            MethodSignature signature = (MethodSignature) thisJoinPoint.getSignature();
            Parameter[] parameters = signature.getMethod()
                .getParameters();
            String message = IntStream.range(0, parameters.length)
                .filter(i -> this.isLoggable(parameters[i]))
                .<String>mapToObj(i -> toString(parameters[i], thisJoinPoint.getArgs()[i]))
                .collect(Collectors.joining(", ", 
                        "method execution " + signature.getName() + "(", ")"));
            Logger methodLogger = LoggerFactory.getLogger(
                    thisJoinPointStaticPart.getSignature().getDeclaringType());
            methodLogger.debug(message);
        }
    
        private boolean isLoggable(Parameter parameter) {
            return parameter.getAnnotation(SkipLogging.class) == null;
        }
    
        private String toString(Parameter parameter, Object value) {
            ToStringWith toStringWith = parameter.getAnnotation(ToStringWith.class);
            if (toStringWith != null) {
                Class<? extends Function<?, String>> converterClass = 
                        toStringWith.value();
                try {
                    @SuppressWarnings("unchecked")
                    Function<Object, String> converter = (Function<Object, String>) 
                        converterClass.newInstance();
                    String str = converter.apply(value);
                    return String.format("%s='%s'", parameter.getName(), str);
                } catch (Exception e) {
                    logger.error("Couldn't instantiate toString converter for logging " 
                            + converterClass.getName(), e);
                    return String.format("%s=<error converting to string>", 
                            parameter.getName());
                }
            } else {
                return String.format("%s='%s'", parameter.getName(), String.valueOf(value));
            }
        }
    
    }
    

    Test code:

    public static class SomethingToStringConverter implements Function<Something, String> {
    
        @Override
        public String apply(Something something) {
            return "Something nice";
        }
    
    }
    
    @Log
    public void test(
            @ToStringWith(SomethingToStringConverter.class) Something something,
            String string, 
            @SkipLogging Class<?> cls, 
            Object object) {
    
    }
    
    public static void main(String[] args) {
    // execution of this method should log the following message:
    // method execution test(something='Something nice', string='some string', object='null')
        test(new Something(), "some string", Object.class, null);
    }
    

    I used Java 8 Streams API in my answer for it's compactness, you could convert the code to normal Java code if you don't use Java 8 features or need better efficiency. It's just to give you an idea.