Skip to content

Commit

Permalink
spring-projectsGH-214: Retryable on JPA Repos with proxyTargetClass
Browse files Browse the repository at this point in the history
Resolves spring-projects#214

When `proxyTargetClass` is `true` (Boot's default), the JPA interfaces
are not copied to the outer retry proxy created by auto-proxy.

Add a BPP to fix up the proxy, when needed.

Also, the pointcut matchers must match on the `SimpleJpaRepository`.
  • Loading branch information
garyrussell committed Jun 8, 2021
1 parent 83a73d0 commit 160ea1a
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 2 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,13 @@ line to your `build.gradle` file:
runtime('org.aspectj:aspectjweaver:1.8.13')
```

### Using Declarative Retry on Spring JPA Repository Interfaces

When using Spring Spring Boot, to enable support for adding `@Retryable` to JPA Repository Interface methods, you can add
a `JPARepositoryRetryBeanPostProcessor` to the application context, or set the [spring.aop.proxy-target-class](https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.core.spring.aop.proxy-target-class) property to `false`.
When this property is true, it prevents the auto-proxy mechanism from copying the repository interfaces to the retry proxy.
Adding the bean post processor causes a new proxy to be created, merging the interfaces and advisors on the JPA and retry proxies.

### XML Configuration

The following example of declarative iteration uses Spring AOP to repeat a service call to
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,12 @@
<version>1.2.17</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.5.1</version>
<optional>true</optional>
</dependency>
</dependencies>

<reporting>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2021 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
*
* https://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.retry.annotation;

import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation;
import org.springframework.retry.interceptor.Retryable;

/**
* Add a bean of this instance if you wish to annotate JPA Repository interfaces with
* {@code @Retryable}. It merges the repository proxy advisors with the retry advisor.
*
* @author Gary Russell
* @since 1.3.2
*/
public class JPARepositoryRetryBeanPostProcessor implements BeanPostProcessor, Ordered {

@Override
public int getOrder() {
return Integer.MAX_VALUE;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Retryable && bean instanceof Advised) {
Advised advised = (Advised) bean;
try {
Object target = advised.getTargetSource().getTarget();
if (target instanceof Advised) {
Advised advised2 = (Advised) target;
Object target2 = advised2.getTargetSource().getTarget();
ProxyFactory pf = new ProxyFactory(target2);
pf.removeInterface(JpaRepositoryImplementation.class);
for (Advisor advisor : advised.getAdvisors()) {
pf.addAdvisor(advisor);
}
for (Advisor advisor : advised2.getAdvisors()) {
pf.addAdvisor(advisor);
}
for (Class<?> iface : advised2.getProxiedInterfaces()) {
pf.addInterface(iface);
}
return pf.getProxy();
}
}
catch (Exception e) {
e.printStackTrace();
}
}
return bean;
}

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ private final class AnnotationClassOrMethodPointcut extends StaticMethodMatcherP

@Override
public boolean matches(Method method, Class<?> targetClass) {
return getClassFilter().matches(targetClass) || this.methodResolver.matches(method, targetClass);
return "org.springframework.data.jpa.repository.support.SimpleJpaRepository".equals(targetClass.getName())
|| getClassFilter().matches(targetClass) || this.methodResolver.matches(method, targetClass);
}

@Override
Expand Down Expand Up @@ -231,7 +232,8 @@ private final class AnnotationClassOrMethodFilter extends AnnotationClassFilter

@Override
public boolean matches(Class<?> clazz) {
return super.matches(clazz) || this.methodResolver.hasAnnotatedMethods(clazz);
return "org.springframework.data.jpa.repository.support.SimpleJpaRepository".equals(clazz.getName())
|| super.matches(clazz) || this.methodResolver.hasAnnotatedMethods(clazz);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.junit.Test;

import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.DirectFieldAccessor;
Expand Down Expand Up @@ -255,6 +256,14 @@ public void testExpression() throws Exception {
context.close();
}

@Test
public void wrappedproxy() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
WrappedProxyConfiguration.class);
context.getBean(TheInterface.class).service2();
assertTrue(context.getBean(WrappedProxyConfiguration.class).innerAdviceCalled);
}

private Object target(Object target) {
if (!AopUtils.isAopProxy(target)) {
return target;
Expand Down Expand Up @@ -481,6 +490,35 @@ public NotAnnotatedInterface notAnnotatedInterface() {

}

@Configuration
@EnableRetry(proxyTargetClass = true)
public static class WrappedProxyConfiguration {

boolean innerAdviceCalled;

@Bean
TheInterface service() {
ProxyFactory pf = new ProxyFactory(new TheClass());
pf.addInterface(TheInterface.class);
pf.addAdvice(new MethodInterceptor() {

@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
WrappedProxyConfiguration.this.innerAdviceCalled = true;
return invocation.proceed();
}

});
return (TheInterface) pf.getProxy();
}

@Bean
static JPARepositoryRetryBeanPostProcessor bpp() {
return new JPARepositoryRetryBeanPostProcessor();
}

}

protected static class Service {

private int count = 0;
Expand Down

0 comments on commit 160ea1a

Please sign in to comment.