/* * Copyright 2016-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.repository.core.support; import lombok.RequiredArgsConstructor; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; import java.util.Map; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.framework.ProxyFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.AfterDomainEventPublication; import org.springframework.data.domain.DomainEvents; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.util.AnnotationDetectionMethodCallback; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ReflectionUtils; /** * {@link RepositoryProxyPostProcessor} to register a {@link MethodInterceptor} to intercept the * {@link CrudRepository#save(Object)} method and publish events potentially exposed via a method annotated with * {@link DomainEvents}. If no such method can be detected on the aggregate root, no interceptor is added. Additionally, * the aggregate root can expose a method annotated with {@link AfterDomainEventPublication}. If present, the method * will be invoked after all events have been published. * * @author Oliver Gierke * @author Christoph Strobl * @since 1.13 * @soundtrack Henrik Freischlader Trio - Master Plan (Openness) */ @RequiredArgsConstructor public class EventPublishingRepositoryProxyPostProcessor implements RepositoryProxyPostProcessor { private final ApplicationEventPublisher publisher; /* * (non-Javadoc) * @see org.springframework.data.repository.core.support.RepositoryProxyPostProcessor#postProcess(org.springframework.aop.framework.ProxyFactory, org.springframework.data.repository.core.RepositoryInformation) */ @Override public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) { EventPublishingMethod method = EventPublishingMethod.of(repositoryInformation.getDomainType()); if (method == null) { return; } factory.addAdvice(new EventPublishingMethodInterceptor(method, publisher)); } /** * {@link MethodInterceptor} to publish events exposed an aggregate on calls to a save method on the repository. * * @author Oliver Gierke * @since 1.13 */ @RequiredArgsConstructor(staticName = "of") static class EventPublishingMethodInterceptor implements MethodInterceptor { private final EventPublishingMethod eventMethod; private final ApplicationEventPublisher publisher; /* * (non-Javadoc) * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) */ @Override public Object invoke(MethodInvocation invocation) throws Throwable { Object result = invocation.proceed(); if (!invocation.getMethod().getName().startsWith("save")) { return result; } for (Object argument : invocation.getArguments()) { eventMethod.publishEventsFrom(argument, publisher); } return result; } } /** * Abstraction of a method on the aggregate root that exposes the events to publish. * * @author Oliver Gierke * @since 1.13 */ @RequiredArgsConstructor static class EventPublishingMethod { private static Map<Class<?>, EventPublishingMethod> CACHE = new ConcurrentReferenceHashMap<>(); private static EventPublishingMethod NONE = new EventPublishingMethod(null, null); private final Method publishingMethod; private final Method clearingMethod; /** * Creates an {@link EventPublishingMethod} for the given type. * * @param type must not be {@literal null}. * @return an {@link EventPublishingMethod} for the given type or {@literal null} in case the given type does not * expose an event publishing method. */ public static EventPublishingMethod of(Class<?> type) { Assert.notNull(type, "Type must not be null!"); EventPublishingMethod eventPublishingMethod = CACHE.get(type); if (eventPublishingMethod != null) { return eventPublishingMethod.orNull(); } AnnotationDetectionMethodCallback<DomainEvents> publishing = new AnnotationDetectionMethodCallback<>( DomainEvents.class); ReflectionUtils.doWithMethods(type, publishing); // TODO: Lazify this as the inspection might not be needed if the publishing callback didn't find an annotation in // the first place AnnotationDetectionMethodCallback<AfterDomainEventPublication> clearing = new AnnotationDetectionMethodCallback<>( AfterDomainEventPublication.class); ReflectionUtils.doWithMethods(type, clearing); EventPublishingMethod result = from(publishing, clearing); CACHE.put(type, result); return result.orNull(); } /** * Publishes all events in the given aggregate root using the given {@link ApplicationEventPublisher}. * * @param object can be {@literal null}. * @param publisher must not be {@literal null}. */ public void publishEventsFrom(Object object, ApplicationEventPublisher publisher) { if (object == null) { return; } for (Object aggregateRoot : asCollection(object)) { for (Object event : asCollection(ReflectionUtils.invokeMethod(publishingMethod, aggregateRoot))) { publisher.publishEvent(event); } } if (clearingMethod != null) { ReflectionUtils.invokeMethod(clearingMethod, object); } } /** * Returns the current {@link EventPublishingMethod} or {@literal null} if it's the default value. * * @return */ private EventPublishingMethod orNull() { return this == EventPublishingMethod.NONE ? null : this; } /** * Creates a new {@link EventPublishingMethod} using the given pre-populated * {@link AnnotationDetectionMethodCallback} looking up an optional clearing method from the given callback. * * @param publishing must not be {@literal null}. * @param clearing must not be {@literal null}. * @return */ private static EventPublishingMethod from(AnnotationDetectionMethodCallback<?> publishing, AnnotationDetectionMethodCallback<?> clearing) { if (!publishing.hasFoundAnnotation()) { return EventPublishingMethod.NONE; } Method eventMethod = publishing.getMethod(); ReflectionUtils.makeAccessible(eventMethod); return new EventPublishingMethod(eventMethod, getClearingMethod(clearing)); } /** * Returns the {@link Method} supposed to be invoked for event clearing or {@literal null} if none is found. * * @param clearing must not be {@literal null}. * @return */ private static Method getClearingMethod(AnnotationDetectionMethodCallback<?> clearing) { if (!clearing.hasFoundAnnotation()) { return null; } Method method = clearing.getMethod(); ReflectionUtils.makeAccessible(method); return method; } /** * Returns the given source object as collection, i.e. collections are returned as is, objects are turned into a * one-element collection, {@literal null} will become an empty collection. * * @param source can be {@literal null}. * @return */ @SuppressWarnings("unchecked") private static Collection<Object> asCollection(Object source) { if (source == null) { return Collections.emptyList(); } if (Collection.class.isInstance(source)) { return (Collection<Object>) source; } return Collections.singletonList(source); } } }