Improving Readability of Java Code with Aspect-Oriented Programming (AOP)

Introduction

When many of us first started learning to write code, we did so in a linear fashion (top to bottom). As we got better our applications began to have more dimensions, with objects and layers. Our ways of thinking are expanded, and in many cases the frameworks we use help guide us in thinking differently and writing better code. What helps make the code better is clean, readable logic that uses to object-oriented principles, and this is where Aspect-Oriented Programming comes in.

LoggerAspect

Here a pointcut is defined, identifying calls to all public methods in the controller and service packages using the PointCut annotation. It is used twice. The first is to ensure that any method within the controller or service packages is logged. The second is to implement trace logging so that the entry and exit calls to each public method is logged with a trace statement.

/**
* The logger aspect class
*/
@Aspect
public class LoggerAspect {
/**
* Constructor
*/
public LoggerAspect(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
private ObjectMapper objectMapper; /**
* A "point cut" cut defines a predicate used to identify a
* collection of join points.
* The the annotation below currently identifies all public
* methods on the controller and service packages.
*/
@Pointcut("within(com.ibm.github..controller..*) || within(com.ibm.github..service..*)")
public void applicationPointCut() {
}
/**
* After Throwing - Advice to take when exceptions are thrown
*
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "applicationPointCut()", throwing = "e")
public void logException(JoinPoint joinPoint, Throwable e) {
this.getLogger(joinPoint).error("Exception in {}() with Cause = {} and message {} ", joinPoint.getSignature().getName(), e.getCause() != null ? e.getCause() : "Null Cause", e.getMessage(), e);
}
/**
* Log Advice - Around advice to take when entering and exiting
* a method
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("applicationPointCut()")
public Object logAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
Logger logger = this.getLogger(joinPoint);
logger.trace("Entered: {} () with arguments = {}" , joinPoint.getSignature().getName(), Arrays.deepToString(joinPoint.getArgs()));
try {
Object object = joinPoint.proceed();
logger.trace("Exited: {} () with result = {}", joinPoint.getSignature().getName(), objectMapper.writeValueAsString(object));
return object;
} catch( Exception e) {
logger.error("Exception {} in {}()", Arrays.toString(joinPoint.getArgs()), joinPoint.getSignature().getName());
throw e;
}
}
/**
* Gets the logger
* package protected to facilitate override in unit tests
* @param joinPoint
* @return
*/
Logger getLogger(JoinPoint joinPoint) {
return LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringTypeNam e());
}
}

RepositoryAspect

Another cross-cutting concern is of persistence layer exception handling. We often want to catch platform exceptions that occur in our persistence layer and return them as more generic exceptions. This can help us keep prevent the leaking of details relating to the implementation of platforms and products we have chosen to use. The following aspect catches any exceptions thrown from our Spring JPA Repository and re-throws them as a more generic PersistenceException, with a generic message.

/**
* The Repository Aspect class
*/
@Aspect
public class RepositoryAspect {
/**
* Constructor
*/
public RepositoryAspect(MessageSource messageSource) {
this.messageSource = messageSource;
}
private MessageSource messageSource; @Pointcut("execution(* org.springframework.data.jpa.repository.JpaRepository+.*(..))")
public void repositoryPointCut() {
}
@AfterThrowing(pointcut = "repositoryPointCut()", throwing = "e")
public void convertToPersistenceException(JoinPoint joinPoint, Throwable e) {
String message = messageSource.getMessage("repositoryaspect.persistenceexception.message", null, LocaleContextHolder.getLocale());
throw new PersistenceException(message, e);
}
}

Improved Readability

How does the use of a LoggerAspect and RepositoryAspect improve readability? Take a look at the ItemsServiceImpl class shown below. Hopefully, the code below comes across to you as being readable. If we didn’t leverage the LoggerAspect and RepsitoryAspect defined above it would need to have logging and try/catch statements inline, and across each method that makes calls to the database. Here the logic is simple and concise, with logging and exception conversion handled by our aspects and other exit exception handling that is built into the framework.

/**
* The Class ItemsServiceImpl.
*/
@Service
@Validated
public class ItemsServiceImpl implements ItemsService {
public static final Logger logger = LoggerFactory.getLogger(ItemsServiceImpl.class);
private ItemRepository itemRepository;
private MessageSource messageSource;
/**
* Constructor
* @param itemRepository
* @param messageSource
*/
public ItemsServiceImpl(ItemRepository itemRepository, MessageSource messageSource) {
this.itemRepository = itemRepository;
this.messageSource = messageSource;
}
/**
* Gets the items.
* @param pageable the pageable
* @return the items
*/
@Override
@Compliance(action = ComplianceAction.read)
public Page<ItemDto> getItems(Pageable pageable){
Page<ItemEntity> page = itemRepository.findAll(pageable);
return page.map(obj -> convert(obj));
}
/**
* Gets the item by id.
* @param id the id
* @return the item by id
*/
@Override
@Compliance(action = ComplianceAction.read)
public Optional<ItemDto> getItemById(long id){
Optional<ItemEntity> optionalEntity = itemRepository.findById(id);
return optionalEntity.map(entity -> Optional.of(convert(entity))).orElse(Optional.empty());
}
...

Join Points identified by an Annotation

On the public methods in the code above, did you notice the “Compliance” annotation? This is a custom annotation made specifically for us to use in conjunction with a custom aspect. In our case, whenever a method that is marked with the Compliance annotation completes, we want something else to be triggered (maybe an event posted posted to a message bus).

/**
* The compliance annotation
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Compliance {
/**
* Action.
* @return the compliance action enum value
*/
ComplianceAction action();
}
/**
* The Class ComplianceEventAspect.
*/
@Aspect
public class ComplianceEventAspect {
/**
* Log compliance event from a new thread
* @param joinPoint the join point
* @param compliance the compliance
*/
@After("@annotation(compliance)")
public void logComplianceEvent(JoinPoint joinPoint, Compliance compliance) {
CompletableFuture.runAsync(() -> {
ComplianceEvent complianceEvent = buildComplianceEvent(joinPoint, compliance);
// TODO: go do something to record the compliance event
});
}
/**
* Builds the compliance event.
* @param joinPoint the join point
* @param compliance the compliance
* @return the compliance event
*/
private ComplianceEvent buildComplianceEvent(JoinPoint joinPoint, Compliance compliance) {
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
ComplianceEvent complianceEvent = new ComplianceEvent();
complianceEvent.setAction(compliance.action());
complianceEvent.setResource(className);
complianceEvent.setEventSource(className + "." + methodName);
return complianceEvent;
}
}

Wiring in the Aspects

To this point, we have skipped the configuration and initialization of the aspects of application startup. For the example, below we are initializing the aspect code as Spring beans.

/**
* The Class AspectConfig.
*/
@Configuration
@EnableAspectJAutoProxy
public class AspectConfig {
private ObjectMapper objectMapper;
private MessageSource messageSource;
public AspectConfig(ObjectMapper objectMapper, MessageSource messageSource) {
this.objectMapper = objectMapper;
this.messageSource = messageSource;
}
/**
* Logger aspect bean.
* @param environment the environment
* @return the logger aspect
*/
@Bean
public LoggerAspect loggerAspect(Environment environment) {
return new LoggerAspect(objectMapper);
}
/**
* Repository aspect bean.
* @param environment the environment
* @return the repository aspect
*/
@Bean
public RepositoryAspect repositoryAspect(Environment environment){
return new RepositoryAspect(messageSource);
}
/**
* Compliance event bean.
* @param environment the environment
* @return the compliance event aspect
*/
@Bean
public ComplianceEventAspect complianceEvent(Environment environment) {
return new ComplianceEventAspect();
}
}

Unit Testing

Unit testing of the aspect logic is performed by wiring up mocks and using the AspectJProxyFactory as shown in the example below.

/**
* RepositoryAspect Unit Tests
*/
@ExtendWith(MockitoExtension.class)
@WebAppConfiguration
@DisplayName("RepositoryAspect Unit Tests")
public class RepositoryAspectTest {
private JpaRepository<ItemEntity, Long> jpaRepositoryMock;
private JpaRepository<ItemEntity, Long> jpaRepositoryProxy;
private MessageSource messageSourceMock;

/**
* Setup.
*/
@SuppressWarnings("unchecked")
@BeforeEach
void setup() {
jpaRepositoryMock = Mockito.mock(JpaRepository.class);
messageSourceMock = Mockito.mock(MessageSource.class);
RepositoryAspect repositoryAspect = new RepositoryAspect(messageSourceMock);
AspectJProxyFactory factory = new AspectJProxyFactory(jpaRepositoryMock);
factory.addAspect(repositoryAspect);
jpaRepositoryProxy = factory.getProxy();
}
/**
* Given call to JPA Repository
*/
@Nested
@DisplayName("Given call to JPA Repository")
class GivenCallToJpaRepository {
/**
* When exception is thrown
*/
@Nested
@DisplayName("When exception is thrown")
class WhenExceptionIsThrown{
private String expectedMessage = "Fake exception message";
/**
* Setup.
*/
@BeforeEach
void setup() {
Mockito.when(jpaRepositoryMock.findById(Mockito.anyLong())).thenThrow( new RuntimeException());
Mockito.when(messageSourceMock.getMessage(Mockito.anyString(), Mockito.any(), Mockito.any(Locale.class))).thenReturn(expectedMessage);
} /**
* Then should throw persistence exception.
*/
@Test
@DisplayName("Then should throw persistence exception") public void thenShouldThrowPersistenceException() {
Exception exception = assertThrows(PersistenceException.class, () -> {
jpaRepositoryProxy.findById(123L);
});
assertTrue(exception.getMessage().contains(expectedMessage))
}
}
}
}

Final Thoughts

The framework you are using may already have ways to address some of the cross-cutting concerns. Before writing code to address some of these concerns, try to become familiar with what your framework provides. Spring boot REST microservices has a pattern for implementing an Exception Handler and associating it with the controller using ControllerAdvice. This allows for errors to bubble-up and is transformed on the way out. Request logging can be turned on by setting the logging level for org.springframework.web.filter.CommonsRequestLoggingFilter to DEBUG and implementing a CommonsRequestLoggingFilter bean.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Mike Hepfer

Mike Hepfer

Full-Stack software developer working with cloud-native technologies.