Skip to content

alpa-coder/jpa-audit-jms

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Using JMS Queue to audit JPA entity reads with Spring!

Recently a colleague of mine (Rudy De Busscher) posted an interesting blog item about using a JMS queue to audit JPA entity reads. After reading this post, I was wondering how I would solve it using Spring.

Problem description

We have a quite simple problem: every time a JPA entity read occurs, this "read" is published to a JMS Queue.

  1. An entity Read happens
  2. Push read message on JMS Queue
  3. Read JMS message and output to stdout

problem description

Solution

There are of course multiple solutions to this problem. In this solution, we will still be using the EntityListener that was shown in the blogpost of Rudy De Busscher. This entity listener will publish a read event. An event listener will pick up the event and publish it to a JMS Queue using a JMS Template. A JMS Message listener will pick up the message and log it to stdout.

A quick overview:

  1. An entity read happens
  2. This entity read triggers an EntityListener
  3. This EntityListener (Audit) publishes a "ReadEvent" using spring events
  4. The (spring) event listener "ReadEventListener" picks up this event and publishes it to a JMS Queue
  5. JMS Queue listener handles this JMS Message and logs it to stdout (normally a different application would handle it)

solution description

As you may notice step 3 and 4 are actually not necessary, we could immediately let the entity listener publish its event to the JMS queue but I wanted to show the improved application events in Spring. And if we ever need to log a different kind of ReadEvent (ex. a NoSql read event), we simple have to fire the necessary event for it and we do not have to care anymore about or how it needs to be logged. Imagine we ever want to change the way ReadEvents are published (JMS), we only have to change it in one place (SoC).

For this, we will be using Spring Boot with the latest Spring version 4.2.0-SNAPSHOT.

Basic settings

We will use a basic Spring Boot application for setup. So the standard "spring-boot-starter-parent" is used with some extra dependencies:

  • spring-boot-starter-test (for basic testing)
  • spring-boot-starter-data-jpa (spring data for performing reads)
  • spring-jms (read/write JMS Queue)
  • activemq-broker (JMS broker)
  • h2 (database)

The only special feature we will add is that we will use Spring 4.2 as Spring version. For this we add the spring-snapshots repository and set the spring.version maven property to: 4.2.0-SNAPSHOT.

Full POM is available on github.

Standard configuration:

@SpringBootApplication
public class AppConfig { }

Entity read

We have a basic entity (Employee) which we can annotate with an EntityListener.

@Entity
@EntityListeners({Audit.class})
public class Employee implements Serializable {
...

Implementation of the entity listener Audit:

public class Audit {
    @PostLoad
    public void auditUsage(Object entity) {
        
    }
}

Publish read event

We will publish a "ReadEvent" object that will carry information about the entity being read:

public class ReadEvent {
    private Object entity;

    public ReadEvent(Object entity) {
        this.entity = entity;
    }

    public Object getEntity() {
        return entity;
    }
}

For publishing the read event, we will use Spring's application events, ie. ApplicationEventPublisher. So we will use basic event handling from Spring (which might not be known by a lot of people.

public class Audit {
    @PostLoad
    public void auditUsage(Object entity) {
        ContextHelper.getPublisher().publishEvent(new ReadEvent(entity));
    }
}

Because this Audit class is not a Spring component, a small ContextHelper class was created to get access to some Spring beans (like the application event publisher).

Consume read event

Now we can use Spring's 4.2 improved application event listeners:

@Component
public class ReadEventListener {
    @Autowired
    private JmsTemplate jmsTemplate;

    @Value("${spring.jms.queue.destination}")
    private String destination;

    @EventListener
    public void onRead(ReadEvent readEvent) {
        jmsTemplate.convertAndSend(destination, readEvent.getEntity());
    }
}

We have one method here "onRead" with our "ReadEvent" object as parameter. We simply need to annotate it with @EventListener and Spring will listen to all application events and when a "ReadEvent" is triggered, this method is executed. This way of event handling is new in Spring 4.2 (that's why I am using a snapshot repository). If you need an older version of Spring, you can still use the standard ApplicationListener.

Publish entity to JMS queue

In the method of the entity listener, we use a JmsTemplate to send our object being read to a JMS Queue. You may notice that we didn't configure anything for this, thank you Spring Boot! Spring Boot notices that I have JMS on the classpath and ActiveMQ, it will start an in memory active MQ and create a JMS template bean which you can autowire. Pretty sweet that we will be having a running application without any confiugration. We can see this when starting the application:

2015-03-29 20:19:32.981  INFO 21950 - [main] o.apache.activemq.broker.BrokerService   : Using Persistence Adapter: MemoryPersistenceAdapter
2015-03-29 20:19:33.188  INFO 21950 - [main] o.a.activemq.broker.TransportConnector   : Connector vm://localhost started

I added one configuration myself, the destination:

spring.jms.queue.destination=audit-queue

At this point, every entity being read by JPA, will be sent to a JMS queue.

Consume JMS queue

For consuming the JMS queue, we will use a standard JMS listener container provided by Spring. We will create a Message Listener Adapter and log every read to stdout. Configuration for the JMS listener container:

@Configuration
public class JmsConfig {
    @Value("${spring.jms.queue.destination}")
    private String destination;

    @Bean
    public MessageListenerAdapter adapter(AuditLogger auditLogger) {
        MessageListenerAdapter messageListener
                = new MessageListenerAdapter(auditLogger);
        messageListener.setDefaultListenerMethod("log");
        return messageListener;
    }

    @Bean
    public SimpleMessageListenerContainer container(MessageListenerAdapter messageListener,
                                             ConnectionFactory connectionFactory) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setMessageListener(messageListener);
        container.setConnectionFactory(connectionFactory);
        container.setDestinationName(destination);
        return container;
    }
}

And finally the implementation of the object responsible for the actual logging:

@Component
public class AuditLogger {

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    public void log(Object entity) {
        log.debug(entity.toString());
    }
}

Logger is configured by spring boot, you can simply change the log level in your application.properties:

logging.level.be.c4j.springsquad=DEBUG

Run application

Since we are using Spring Boot, we can simply start the application, create some users and read them:

public class App {

    private static List<String> names = Arrays.asList(
            "Davy Van Roy",
            "Stefanie Jacobs",
            "Amélie Van Roy",
            "Lucas Van Roy"
    );

    public static void main(String[] args) {
        ConfigurableApplicationContext context = createContext(args);
        auditUsers(context);
    }

    private static ConfigurableApplicationContext createContext(String[] args) {
        ConfigurableApplicationContext context = new SpringApplicationBuilder()
                .sources(AppConfig.class)
                .run(args);
        context.registerShutdownHook();
        return context;
    }

    private static void auditUsers(ConfigurableApplicationContext context) {
        EmployeeRepository repository = context.getBean(EmployeeRepository.class);
        createUsers(repository);
        readUsers(repository);
    }

    private static void createUsers(EmployeeRepository repository) {
        names.stream()
                .map(Employee::new)
                .forEach(repository::save);
    }

    private static void readUsers(EmployeeRepository repository) {
        names.stream()
                .forEach(repository::findByName);
    }
}

And we have success according to the output:

2015-03-29 21:44:41.486 DEBUG 22169 --- [ Session Task-1] b.c.s.infrastructure.audit.AuditLogger   : Employee{id=1, name='Davy Van Roy'}
2015-03-29 21:44:41.493 DEBUG 22169 --- [ Session Task-1] b.c.s.infrastructure.audit.AuditLogger   : Employee{id=2, name='Stefanie Jacobs'}
2015-03-29 21:44:41.498 DEBUG 22169 --- [ Session Task-1] b.c.s.infrastructure.audit.AuditLogger   : Employee{id=3, name='Amélie Van Roy'}
2015-03-29 21:44:41.504 DEBUG 22169 --- [ Session Task-1] b.c.s.infrastructure.audit.AuditLogger   : Employee{id=4, name='Lucas Van Roy'}

Conclusion

We were able to use standard functionality from Spring to solve the problem, and it was pretty straight forward as well. The pieces of the puzzle fit nicely together. Using Spring Boot we were able to set up the environment in a matter of minutes, which is pretty sweet.

If you really want to know more details, please take a look at the source code. Only parts of the code were shown in this blogpost to avoid clutter (but actually, there is not a lot of code).

References

About

Audit JPA entity reads into a JMS queue using Spring

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages