Spring / AOP: Best way to implement an activities

2020-07-22 19:28发布

问题:

I have been going through some Spring / AOP tutorials and have somewhat familiarized myself with the related concepts.

Now coming to my requirements, I need to create an Activities Log implementation which will save the activities of a logged-in user in the DB which can range from applying for a service or creating new users in case of Admin users, etc. On invocation of any method having an annotation (say @ActivityLog), this information is to be persisted in the form of actorId, actionComment, actionTime, actedUponId, ... etc.

Now, if I create a POJO class (that maps to a ActivityLog table in the DB) and want to save this data from inside the Advice (preferably using the same transaction as the method, method uses @Transactional annotation), how do I actually populate the variables in this POJO?? I can probably get the actorId from the session object & actionTime can simply be new Date() but how about the dynamic values for actionComment / actedUponId?

Any help will be brilliant! (BTW, I have a requirement to not use Hibernate Interceptors.)

回答1:

Here is a complete example:

@Aspect
@Component
public class WebMethodAuditor {

protected final Log logger = LogFactory.getLog(getClass());

public static final String DATE_FORMAT_NOW = "yyyy-MM-dd HH:mm:ss";

@Autowired
AuditRecordDAO auditRecordDAO; 

@Before("execution(* com.mycontrollers.*.*(..))")
public void beforeWebMethodExecution(JoinPoint joinPoint) {
    Object[] args = joinPoint.getArgs();
    String methodName = joinPoint.getSignature().getName();
    User principal = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    Timestamp timestamp = new Timestamp(new java.util.Date().getTime());
    // only log those methods called by an end user
    if(principal.getUsername() != null) {
        for(Object o : args) {
            Boolean doInspect = true;
            if(o instanceof ServletRequestDataBinder) doInspect = false;
            if(o instanceof ExtendedModelMap) doInspect = false;
            if(doInspect) {
                if(o instanceof BaseForm ) {
                    // only show form objects
                    AuditRecord ar = new AuditRecord();
                    ar.setUsername(principal.getUsername());
                    ar.setClazz(o.getClass().getCanonicalName());
                    ar.setMethod(methodName);
                    ar.setAsString(o.toString());
                    ar.setAudit_timestamp(timestamp);
                    auditRecordDAO.save(ar);
                }
            }
        }
    }
}

}


回答2:

If you are looking to get the actionComment and actedUponId from arguments to the annotated method (assuming they're both strings), you can add binding terms to your @Around pointcut like this:

@Around("@annotation(ActivityLog) && args(actionComment,actedUponId)")
public Object logActivity(ProceedingJoinPoint pjp,
        String actionComment, String actedUponId) throws Throwable {
    // ... get other values from context, etc. ...
    // ... write to log ...
    pjp.proceed();
}

The args binding in a pointcut can be used in partially-specified mode, in case there are other arguments about that you aren't interested in, and since the aspect is itself a bean, it can be wired into everything else that is going on in the normal way.

Note that if you're mixing declarative transaction management on the same method calls, you've got to get the order of aspects correct. That's done in part by making the aspect bean also implement the Spring Ordered interface, and by controlling the precedence of transactions through the order attribute to <tx:annotation-driven/>. (If that's impossible, you'll be forced to do clever things with direct transaction handling; that's a vastly more painful option to get right…)



回答3:

You will be getting a reference to the org.aspectj.lang.JoinPoint in your advice.You can get the name of target method being executed with toShortString().You can have a loop-up/property file with the method-name=comments entries.This comments can be populated into the POJO.actionComment.method-name can be set to POJO.actedUponId.

I hope the advice should run within the same transaction, if the data-access method is adviced and the service method uses @Transactional.