Skip to content
This repository has been archived by the owner on Jul 26, 2024. It is now read-only.

Commit

Permalink
Merge pull request #13 from ralf-ueberfuhr-ars/features/interceptors
Browse files Browse the repository at this point in the history
Introduce interceptors
  • Loading branch information
ralf-ueberfuhr-ars authored Dec 22, 2023
2 parents db18f58 + 3ace7ba commit 227b5d8
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package de.sample.schulung.spring.blog.domain;

import de.sample.schulung.spring.blog.domain.interceptors.PublishEvent;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

Expand All @@ -17,7 +17,6 @@
@RequiredArgsConstructor
public class BlogPostService {

private final ApplicationEventPublisher eventPublisher;
private final BlogPostSink sink;

public long count() {
Expand All @@ -32,11 +31,10 @@ public Optional<BlogPost> findById(UUID id) {
return sink.findById(id);
}

@PublishEvent(BlogPostCreatedEvent.class)
public void create(@Valid @NotNull BlogPost blogPost) {
blogPost.setTimestamp(LocalDateTime.now());
this.sink.create(blogPost);
this.eventPublisher.publishEvent(new BlogPostCreatedEvent(blogPost));
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ public interface BlogPostSink {
Stream<BlogPost> findAll();
Optional<BlogPost> findById(UUID id);

default void createAll(BlogPost... blogposts) {
Stream.of(blogposts)
.forEach(this::create);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package de.sample.schulung.spring.blog.domain.interceptors;

import java.lang.annotation.*;

/**
* Annotate a method to get an event fired after method execution.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PublishEvent {

/**
* The event class. This class needs a constructor with the same parameters as the method.
*
* @return the event class
*/
Class<?> value();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package de.sample.schulung.spring.blog.domain.interceptors;

import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

@Component
public class PublishEventInterceptor
extends AbstractBeanFactoryAwareAdvisingPostProcessor
implements InitializingBean {

private final MethodInterceptor publishEventAdvice;

public PublishEventInterceptor(final ApplicationEventPublisher eventPublisher) {
this.publishEventAdvice = invocation -> {
// create event object
Object event = null;
// find @PublishEvent annotation on invoked method
final var annotation = AnnotationUtils.findAnnotation(invocation.getMethod(), PublishEvent.class);
if(null != annotation) {
event = annotation.value() // event type
// event type must have a constructor with the same type as the invoked method
// service.create(blogPost) -> new BlogPostCreatedEvent(blogPost)
.getConstructor(invocation.getMethod().getParameterTypes())
.newInstance(invocation.getArguments());
}
// if something is wrong until here, we do not invoke the service's create-method
// now, we invoke the service
final var result = invocation.proceed();
// if an exception occured, the event is not fired
// now, we fire the event
if(null != event) {
eventPublisher.publishEvent(event);
}
// and we need to return the service's result to the invoker (the controller)
return result;
};
}

@Override
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(null, PublishEvent.class, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, publishEventAdvice);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import de.sample.schulung.spring.blog.domain.BlogPostSink;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;
import java.util.UUID;
Expand All @@ -29,6 +30,12 @@ public void create(BlogPost post) {
mapper.copy(savedEntity, post);
}

@Transactional
@Override
public void createAll(BlogPost... blogposts) {
BlogPostSink.super.createAll(blogposts);
}

@Override
public Stream<BlogPost> findAll() {
return repo.findAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import de.sample.schulung.spring.blog.domain.InMemoryBlogPostSink;
import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;
import org.mockito.Mockito;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -14,8 +12,7 @@ public class BlogPostControllerTests {

@Test
void shouldCreateBlogPostSuccessfully() {
final var eventPublisher = Mockito.mock(ApplicationEventPublisher.class);
final var service = new BlogPostService(eventPublisher, new InMemoryBlogPostSink());
final var service = new BlogPostService(new InMemoryBlogPostSink());
final var mapper = Mappers.getMapper(BlogPostDtoMapper.class);
final var controller = new BlogPostController(service, mapper);
final var blogPost = new BlogPostDto();
Expand Down

0 comments on commit 227b5d8

Please sign in to comment.