Using Custom Annotations and AOP for Logging and Parameter Validation in Spring
This article explains how to create and use custom Java annotations together with Spring AOP to implement unified logging, method‑level parameter validation, and other cross‑cutting concerns, providing practical code examples and best‑practice recommendations for backend developers.
Java provides built‑in annotations such as @Override , @Autowired , and @Service , but many developers only use them without understanding how annotations work or how to create their own.
Annotations are divided into meta‑annotations (annotations that describe other annotations) and custom annotations. The four standard meta‑annotations in the JDK are @Target , @Retention , @Documented , and @Inherited ; all other annotations are custom.
Logging with a custom annotation
By defining a custom annotation OpLog and an AOP aspect, you can automatically record operation type, item name, and a unique identifier for any method annotated with @OpLog . The annotation definition is:
/**
* Operate Log custom annotation
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OpLog {
public OpType opType();
public String opItem();
public String opItemIdExpression();
}The corresponding aspect extracts the annotation, evaluates a SpEL expression to obtain the item ID, and logs the information:
/**
* OpLog aspect for logging
*/
@Aspect
@Component
public class OpLogAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(OpLogAspect.class);
@Autowired
HttpServletRequest request;
@Around("@annotation(com.hollis.annotation.OpLog)")
public Object log(ProceedingJoinPoint pjp) throws Exception {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
OpLog opLog = method.getAnnotation(OpLog.class);
Object response = null;
try {
response = pjp.proceed();
} catch (Throwable t) {
throw new Exception(t);
}
if (StringUtils.isNotEmpty(opLog.opItemIdExpression())) {
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(opLog.opItemIdExpression());
EvaluationContext context = new StandardEvaluationContext();
Object[] args = pjp.getArgs();
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(method);
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
}
if (response != null) {
context.setVariable(
CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, response.getClass().getSimpleName()),
response);
}
String itemId = String.valueOf(expression.getValue(context));
handle(opLog.opType(), opLog.opItem(), itemId);
}
return response;
}
private void handle(OpType opType, String opItem, String opItemId) {
LOGGER.info("opType = " + opType.name() + ",opItem = " + opItem + ",opItemId = " + opItemId);
}
}Applying the annotation to a controller method automatically logs the operation:
@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
@OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#id")
public @ResponseBody HashMap view(@RequestParam(name = "id") String id) throws Exception {
// method body
}Parameter validation with a custom annotation
A bean class can declare validation constraints using Hibernate Validator, e.g.:
public class User {
private String idempotentNo;
@NotNull(message = "userName can't be null")
private String userName;
}A utility class validates any bean and throws ValidationException on failure:
/**
* Parameter validation utility
*/
public class BeanValidator {
private static Validator validator = Validation.byProvider(HibernateValidator.class)
.configure().failFast(true).buildValidatorFactory().getValidator();
public static void validateObject(Object object, Class
... groups) throws ValidationException {
Set
> violations = validator.validate(object, groups);
if (violations.stream().findFirst().isPresent()) {
throw new ValidationException(violations.stream().findFirst().get().getMessage());
}
}
}A marker annotation @Facade is defined to indicate methods that need validation:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Facade {}The corresponding aspect intercepts all @Facade methods, validates each argument, executes the method, and returns a unified failure response if validation or execution throws an exception:
@Aspect
@Component
public class FacadeAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(FacadeAspect.class);
@Autowired
HttpServletRequest request;
@Around("@annotation(com.hollis.annotation.Facade)")
public Object facade(ProceedingJoinPoint pjp) throws Exception {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
Object[] args = pjp.getArgs();
Class returnType = ((MethodSignature) pjp.getSignature()).getMethod().getReturnType();
for (Object param : args) {
try {
BeanValidator.validateObject(param);
} catch (ValidationException e) {
return getFailedResponse(returnType, e);
}
}
try {
Object response = pjp.proceed();
return response;
} catch (Throwable t) {
return getFailedResponse(returnType, t);
}
}
private Object getFailedResponse(Class returnType, Throwable throwable)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
if (returnType.getDeclaredConstructor().newInstance() instanceof BaseResponse) {
BaseResponse response = (BaseResponse) returnType.getDeclaredConstructor().newInstance();
response.setSuccess(false);
response.setResponseMessage(throwable.toString());
response.setResponseCode(GlobalConstant.BIZ_ERROR);
return response;
}
LOGGER.error("failed to getFailedResponse, returnType (" + returnType + ") is not instance of BaseResponse");
return null;
}
}Developers simply add @Facade to any controller method to obtain automatic validation and standardized error handling:
@Facade
public TestResponse query(User user) {
// business logic
}The article concludes that custom annotations combined with AOP dramatically reduce repetitive code, improve maintainability, and enable unified handling of logging, validation, caching, and other cross‑cutting concerns, while cautioning against over‑use.
Full-Stack Internet Architecture
Introducing full-stack Internet architecture technologies centered on Java
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.