As illustrated in the (failing) test below, Declarables are not declared when a connection is created:
private const val QUEUE = "test"
#SpringBootTest
class ThreadChannelConnectionFactoryTest {
#Autowired
private lateinit var admin: AmqpAdmin
#Test
fun queueExists() {
assertThat(admin.getQueueInfo(QUEUE)).isNotNull
}
#SpringBootApplication(proxyBeanMethods = false)
protected class TestApplication {
#Bean
fun queue() = QueueBuilder.nonDurable(QUEUE).build()
#Bean
fun connectionFactory() = ThreadChannelConnectionFactory(RabbitConnectionFactoryBean().rabbitConnectionFactory)
}
}
Commenting out the connectionFactory bean definition makes the test pass, since Spring Boot then creates an old-fashioned CachingConnectionFactory.
Is there any good reason for that or has it simply been overlooked? What are my options if I need both feature sets?
ThreadLocal channels
automatic Declarable declaration
EDIT : The following workaround seems to work as of 2.3.1:
class ConnectionListenerFiringThreadChannelConnectionFactory(connectionFactory: ConnectionFactory, isPublisher: Boolean) : ThreadChannelConnectionFactory(connectionFactory) {
init {
if (!isPublisher) {
setPublisherConnectionFactory(ConnectionListenerFiringThreadChannelConnectionFactory(connectionFactory, true))
}
}
constructor(connectionFactory: ConnectionFactory) : this(connectionFactory, false)
override fun createConnection(): Connection {
val connection = super.createConnection()
connectionListener.onCreate(connection)
return connection
}
}
It's a bug; your work around is correct.
https://github.com/spring-projects/spring-amqp/issues/1268
Related
Spring 6, Quartz, and a SimpleTrigger based scheduled task.
#Component
#Slf4j
public class Greeting {
public void sayHello() {
log.debug("Hello at {}:", LocalDateTime.now());
}
}
Quartz config:
#Configuration
class QuartzConfig{
#Bean
MethodInvokingJobDetailFactoryBean greetingJobDetailFactoryBean() {
var jobFactory = new MethodInvokingJobDetailFactoryBean();
jobFactory.setTargetBeanName("greeting");
jobFactory.setTargetMethod("sayHello");
return jobFactory;
}
#Bean
public SimpleTriggerFactoryBean simpleTriggerFactoryBean() {
SimpleTriggerFactoryBean simpleTrigger = new SimpleTriggerFactoryBean();
simpleTrigger.setJobDetail(greetingJobDetailFactoryBean().getObject());
simpleTrigger.setStartDelay(1_000);
simpleTrigger.setRepeatInterval(5_000);
return simpleTrigger;
}
#Bean
public SchedulerFactoryBean schedulerFactoryBean() {
var factory = new SchedulerFactoryBean();
factory.setTriggers(
simpleTriggerFactoryBean().getObject(),
cronTriggerFactoryBean().getObject()
);
return factory;
}
And I tried to use awaitility to check the invocations.
#SpringJUnitConfig(value = {
QuartzConfig.class,
Greeting.class
})
public class GreetingTest {
#Autowired
Greeting greeting;
Greeting greetingSpy;
#BeforeEach
public void setUp() {
this.greetingSpy = spy(greeting);
}
#Test
public void whenWaitTenSecond_thenScheduledIsCalledAtLeastTenTimes() {
await()
.atMost(Duration.ofSeconds(10))
.untilAsserted(() -> verify(greetingSpy, atLeast(1)).sayHello());
}
}
Running the tests, it is failed.
org.awaitility.core.ConditionTimeoutException: Assertion condition defined as a com.example.demo.GreetingTest
Wanted but not invoked:
greeting.sayHello();
-> at com.example.demo.GreetingTest.lambda$whenWaitTenSecond_thenScheduledIsCalledAtLeastTenTimes$0(GreetingTest.java:36)
Actually, there were zero interactions with this mock.
within 10 seconds.
In the jobDetailFactorBean, I used jobFactory.setTargetBeanName("greeting"); to setup the target beans here, it should pass the Greeting bean directly.
Updated: resolved myself, check here.
You're creating a spy that in no way interacts with the actual code:
#BeforeEach
public void setUp() {
this.greetingSpy = spy(greeting);
}
This would have to be injected into the Spring context as a bean and used everywhere, where greeting is used. Spring actually provides such functionality: #SpyBean.
Instead of autowiring a greeting and wrapping it with a spy that does not interact with anything in the context, replace the #Autowired with #SpyBean annotation. Thanks to that a spy bean will be created and injected within the Spring context:
#SpyBean
Greeting greeting;
I created a commit in GitHub repository, where you can see the whole code - the test passes. I had to add the cronTriggerFactoryBean() method to the configuration as it is omitted in your question.
If you cannot use Spring Boot, you can create the spy within Spring context yourself using configuration:
static class Config {
#Bean
#Primary
Greeting greeting() {
return spy(new Greeting());
}
}
Thanks to that when you inject the bean, it will be possible to act on it with Mockito (remember to include the Config class in the #SpringJUnitConfig annotation).
I created another commit in the GitHub repository - the test passes. You can see the whole code there.
I have written a PropertySource that enables classpath: prefix for spring.kafka.properties.ssl.truststore.location (which out-of-the-box is not supported).
Essentially, this lets me place a truststore.jks inside my Spring Boot application's src/main/resources folder and reference it from inside the .jar file.
This works nicely for plain Spring Kafka configurations, like these:
spring:
kafka:
properties:
ssl.truststore.location: classpath:myTruststore.jks
It currently fails when the same configurations are given in the context of a Spring Cloud Stream Binder:
spring:
cloud:
stream:
binders:
my-binder:
type: kafka
environment:
spring:
kafka:
properties:
ssl.truststore.location: classpath:myTruststore.jks
My PropertySource is not even called back, when I would have expected it to be called with a poperty name of spring.cloud.stream.binders.my-binder.environment.spring.kafka.properties.ssl.truststore.location.
I think my PropertySource that would do the classpath: resolution is not part of the Environment of the given Spring Cloud Stream binder.
Question: how can one add PropertySources to a specific Binder's environment (or to all of them)?
Thanks!
EDIT
I add my PropertySource in a Spring Boot auto-configuration like this:
#Configuration
#AutoConfigureBefore(KafkaAutoConfiguration.class)
#ConditionalOnProperty(name = "com.acme.kafka.enabled", matchIfMissing = true)
#EnableConfigurationPropertiesAcmeKafkaConfigurations.class)
public class AcmeKafkaAutoConfiguration {
#Bean
ClasspathResourceSupportEnablingPropertySource acmeKafkaClasspathResourceEnablingPropertySource(ConfigurableEnvironment environment) throws IOException {
ClasspathResourcesSupport classpathResourcesSupport = new ClasspathResourcesSupport(Files.createTempDirectory(ACME_KAFKA_PREFIX));
ClasspathResourceSupportEnablingPropertySource propertySource
= new ClasspathResourceSupportEnablingPropertySource(ClasspathResourceSupportEnablingPropertySource.NAME, environment, classpathResourcesSupport);
environment.getPropertySources().addFirst(propertySource);
return propertySource;
}
}
EDIT NO.2: I tried out what Gary Russel suggested below (using a Bean Post Processor declared as a static bean method).
It works but in my case I get a lot of additional warning logs at startup of the form:
Bean '...' of type [...] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
According to this post this can cause some really nasty side effects.
Here is the code I was using (which caused the warnings above):
#Configuration
#AutoConfigureBefore(KafkaAutoConfiguration.class)
#ConditionalOnProperty(name = "com.acme.kafka.enabled", matchIfMissing = true)
#EnableConfigurationProperties(AcmeKafkaConfigurations.class)
public class AcmeKafkaAutoConfiguration {
private static final String ACME_KAFKA_PREFIX = "acme.kafka.";
#Bean
#ConditionalOnMissingBean
public static List<ConnectivityConfigurationsProvider> acmeKafkaTokenProviders(OAuth2TokenClient oAuthClient, AcmeKafkaConfigurations configuration) {
List<ConnectivityConfigurationsProvider> connectivityConfigurationsProviders = new ArrayList<>();
configuration.getInstances().forEach(serviceInstanceConfiguration -> {
TokenProvider tokenProvider = new DefaultOAuth2TokenProvider(oAuthClient, serviceInstanceConfiguration);
ConnectivityConfigurationsProvider connectivityConfigurationsProvider = new ConnectivityConfigurationsProvider(serviceInstanceConfiguration, tokenProvider);
connectivityConfigurationsProviders.add(connectivityConfigurationsProvider);
});
return connectivityConfigurationsProviders;
}
#Bean
#ConditionalOnMissingBean
#Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public static OAuth2TokenClient acmeKafkaOAuth2TokenClient() {
return new DefaultOAuth2TokenClient(new DefaultClientCredentialsTokenResponseClient());
}
#Bean
public static ConnectivityConfigurationsProviders acmeKafkaConnectivityConfigurationsProviders(AcmeKafkaConfigurations configuration, List<ConnectivityConfigurationsProvider> connectivityConfigurationsProviders) {
return new ConnectivityConfigurationsProviders(connectivityConfigurationsProviders);
}
#Bean
static NoOpBeanPostProcessor springKafkaConfigurationsPropertySource(ConfigurableEnvironment environment, ConnectivityConfigurationsProviders connectivityConfigurationsProviders) {
SpringKafkaConfigurationsPropertySource propertySource = new SpringKafkaConfigurationsPropertySource(SpringKafkaConfigurationsPropertySource.NAME, connectivityConfigurationsProviders);
environment.getPropertySources().addLast(propertySource);
return new NoOpBeanPostProcessor();
}
#Bean
#ConditionalOnClass(name = "org.springframework.cloud.stream.binder.BinderConfiguration")
static NoOpBeanPostProcessor springCloudStreamKafkaConfigurationsPropertySource(ConfigurableEnvironment environment, ConnectivityConfigurationsProviders connectivityConfigurationsProviders) {
SpringCloudStreamKafkaConfigurationsPropertySource propertySource = new SpringCloudStreamKafkaConfigurationsPropertySource(SpringCloudStreamKafkaConfigurationsPropertySource.NAME, connectivityConfigurationsProviders);
environment.getPropertySources().addLast(propertySource);
return new NoOpBeanPostProcessor();
}
#Bean
static NoOpBeanPostProcessor acmeKafkaConnectivityConfigurationsPropertySource(ConfigurableEnvironment environment, ConnectivityConfigurationsProviders connectivityConfigurationsProviders) {
AcmeKafkaConnectivityConfigurationsPropertySource propertySource = new AcmeKafkaConnectivityConfigurationsPropertySource(AcmeKafkaConnectivityConfigurationsPropertySource.NAME, connectivityConfigurationsProviders);
environment.getPropertySources().addLast(propertySource);
return new NoOpBeanPostProcessor();
}
#Bean
static NoOpBeanPostProcessor acmeKafkaClasspathResourceEnablingPropertySource(ConfigurableEnvironment environment) throws IOException {
ClasspathResourcesSupport classpathResourcesSupport = new ClasspathResourcesSupport(Files.createTempDirectory(ACME_KAFKA_PREFIX));
ClasspathResourceSupportEnablingPropertySource propertySource
= new ClasspathResourceSupportEnablingPropertySource(ClasspathResourceSupportEnablingPropertySource.NAME, environment, classpathResourcesSupport);
environment.getPropertySources().addFirst(propertySource);
return new NoOpBeanPostProcessor();
}
/**
* This BeanPostProcessor does not really post-process any beans.
* It is a way of getting the bean methods that add the property sources
* above to be called early enough in the lifecycle of Spring ApplicationContext
* creation.
*
* BeanPostProcessors are instantiated by Spring extremely early.
* #Bean methods providing them should be declared as static methods.
* See: https://stackoverflow.com/questions/30874244/bean-annotation-on-a-static-method
*/
static class NoOpBeanPostProcessor implements BeanPostProcessor, Ordered {
#Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
}
EDIT
This seems to work:
#SpringBootApplication
public class So61826877Application {
private static final String KEY = "spring.cloud.stream.binders.my-binder.environment."
+ "spring.kafka.properties.ssl.truststore.location";
public static void main(String[] args) {
SpringApplication.run(So61826877Application.class, args);
}
#Bean
public static BeanPostProcessor configureSource(ConfigurableEnvironment env) {
Properties source = new Properties();
System.out.println(env.getProperty(KEY));
source.setProperty(KEY, "/path/to/myTruststore.jks");
env.getPropertySources().addFirst(new PropertiesPropertySource("cp", source));
return new MyBpp();
}
#Bean
Consumer<String> input() {
return System.out::println;
}
}
class MyBpp implements BeanPostProcessor, Ordered {
#Override
public int getOrder() {
return Integer.MAX_VALUE;
}
}
classpath:myTruststore.jks
...
ConsumerConfig values:
allow.auto.create.topics = true
auto.commit.interval.ms = 100
...
ssl.truststore.location = /path/to/myTruststore.jks
...
I have mixins configured in my objectmapperbuilder config, using the regular spring web controller, the data outputted according to the mixins.
However using webflux, a controller with a method returning a Flow or Mono have the data serialized like if the objectmapper a default one.
How to get webflux to enforce an objectmapper configuration to be used ?
sample config:
#Bean
JavaTimeModule javatimeModule(){
return new JavaTimeModule();
}
#Bean
Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(){
return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.mixIn(MyClass.class, MyClassMixin.class);
}
I actually found my solution by stepping through the init code:
#Configuration
public class Config {
#Bean
JavaTimeModule javatimeModule(){
return new JavaTimeModule();
}
#Bean
Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(){
return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.featuresToEnable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.mixIn(MyClass.class, MyClassMixin.class);
}
#Bean
Jackson2JsonEncoder jackson2JsonEncoder(ObjectMapper mapper){
return new Jackson2JsonEncoder(mapper);
}
#Bean
Jackson2JsonDecoder jackson2JsonDecoder(ObjectMapper mapper){
return new Jackson2JsonDecoder(mapper);
}
#Bean
WebFluxConfigurer webFluxConfigurer(Jackson2JsonEncoder encoder, Jackson2JsonDecoder decoder){
return new WebFluxConfigurer() {
#Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().jackson2JsonEncoder(encoder);
configurer.defaultCodecs().jackson2JsonDecoder(decoder);
}
};
}
}
I translated the solution of #Alberto Galiana to Java and injected the configured Objectmapper for convenience, so you avoid having to do multiple configurations:
#Configuration
#RequiredArgsConstructor
public class WebFluxConfig implements WebFluxConfigurer {
private final ObjectMapper objectMapper;
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().jackson2JsonEncoder(
new Jackson2JsonEncoder(objectMapper)
);
configurer.defaultCodecs().jackson2JsonDecoder(
new Jackson2JsonDecoder(objectMapper)
);
}
}
Just implement WebFluxConfigurer and override method configureHttpMessageCodecs
Sample code for Spring Boot 2 + Kotlin
#Configuration
#EnableWebFlux
class WebConfiguration : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.defaultCodecs().jackson2JsonEncoder(Jackson2JsonEncoder(ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)))
configurer.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(ObjectMapper()
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)))
}
}
Make sure all your data classes to be encoded/decoded have all its properties annotated with #JsonProperty even if property name is equal in class and json data
data class MyClass(
#NotNull
#JsonProperty("id")
val id: String,
#NotNull
#JsonProperty("my_name")
val name: String)
In my case, I was trying to use a customized ObjectMapper while inheriting all of the behavior from my app's default WebClient.
I found that I had to use WebClient.Builder.codecs. When I used WebClient.Builder.exchangeStrategies, the provided overrides were ignored. Not sure if this behavior is something specific to using WebClient.mutate, but this is the only solution I found that worked.
WebClient customizedWebClient = webClient.mutate()
.codecs(clientCodecConfigurer ->
clientCodecConfigurer.defaultCodecs()
.jackson2JsonDecoder(new Jackson2JsonDecoder(customObjectMapper)))
.build();
I have tried all the different solutions (#Primary #Bean for ObjectMapper, configureHttpMessageCodecs(), etc.). What worked for me at the end was specifying a MIME type. Here's an example:
#Configuration
class WebConfig: WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
val encoder = Jackson2JsonEncoder(objectMapper, MimeTypeUtils.APPLICATION_JSON)
val decoder = Jackson2JsonDecoder(objectMapper, MimeTypeUtils.APPLICATION_JSON)
configurer.defaultCodecs().jackson2JsonEncoder(encoder)
configurer.defaultCodecs().jackson2JsonDecoder(decoder)
}
}
Is it possible to access beans defined outside of the step scope? For example, if I define a strategy "strategyA" and pass it in the job parameters I would like the #Value to resolve to the strategyA bean. Is this possible? I am currently working round the problem by getting the bean manually from the applicationContext.
#Bean
#StepScope
public Tasklet myTasklet(
#Value("#{jobParameters['strategy']}") MyCustomClass myCustomStrategy)
MyTasklet myTasklet= new yTasklet();
myTasklet.setStrategy(myCustomStrategy);
return myTasklet;
}
I would like to have the ability to add more strategies without having to modify the code.
The sort answer is yes. This is more general spring/design pattern issue rater then Spring Batch.
The Spring Batch tricky parts are the configuration and understanding scope of bean creation.
Let’s assume all your Strategies implement Strategy interface that looks like:
interface Strategy {
int execute(int a, int b);
};
Every strategy should implements Strategy and use #Component annotation to allow automatic discovery of new Strategy. Make sure all new strategy will placed under the correct package so component scan will find them.
For example:
#Component
public class StrategyA implements Strategy {
#Override
public int execute(int a, int b) {
return a+b;
}
}
The above are singletons and will be created on the application context initialization.
This stage is too early to use #Value("#{jobParameters['strategy']}") as JobParameter wasn't created yet.
So I suggest a locator bean that will be used later when myTasklet is created (Step Scope).
StrategyLocator class:
public class StrategyLocator {
private Map<String, ? extends Strategy> strategyMap;
public Strategy lookup(String strategy) {
return strategyMap.get(strategy);
}
public void setStrategyMap(Map<String, ? extends Strategy> strategyMap) {
this.strategyMap = strategyMap;
}
}
Configuration will look like:
#Bean
#StepScope
public MyTaskelt myTasklet () {
MyTaskelt myTasklet = new MyTaskelt();
//set the strategyLocator
myTasklet.setStrategyLocator(strategyLocator());
return myTasklet;
}
#Bean
protected StrategyLocator strategyLocator(){
return = new StrategyLocator();
}
To initialize StrategyLocator we need to make sure all strategy were already created. So the best approach would be to use ApplicationListener on ContextRefreshedEvent event (warning in this example strategy names start with lower case letter, changing this is easy...).
#Component
public class PlugableStrategyMapper implements ApplicationListener<ContextRefreshedEvent> {
#Autowired
private StrategyLocator strategyLocator;
#Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
Map<String, Strategy> beansOfTypeStrategy = applicationContext.getBeansOfType(Strategy.class);
strategyLocator.setStrategyMap(beansOfTypeStrategy);
}
}
The tasklet will hold a field of type String that will be injected with Strategy enum String using #Value and will be resolved using the locator using a "before step" Listener.
public class MyTaskelt implements Tasklet,StepExecutionListener {
#Value("#{jobParameters['strategy']}")
private String strategyName;
private Strategy strategy;
private StrategyLocator strategyLocator;
#BeforeStep
public void beforeStep(StepExecution stepExecution) {
strategy = strategyLocator.lookup(strategyName);
}
#Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
int executeStrategyResult = strategy.execute(1, 2);
}
public void setStrategyLocator(StrategyLocator strategyLocator) {
this.strategyLocator = strategyLocator;
}
}
To attach the listener to the taskelt you need to set it in your step configuration:
#Bean
protected Step myTaskletstep() throws MalformedURLException {
return steps.get("myTaskletstep")
.transactionManager(transactionManager())
.tasklet(deleteFileTaskelt())
.listener(deleteFileTaskelt())
.build();
}
jobParameters is holding just a String object and not the real object (and I think is not a good pratice store a bean definition into parameters).
I'll move in this way:
#Bean
#StepScope
class MyStategyHolder {
private MyCustomClass myStrategy;
// Add get/set
#BeforeJob
void beforeJob(JobExecution jobExecution) {
myStrategy = (Bind the right strategy using job parameter value);
}
}
and register MyStategyHolder as listener.
In your tasklet use #Value("#{MyStategyHolder.myStrategy}") or access MyStategyHolder instance and perform a getMyStrategy().
I'm being hit with the issue that spock doesn't allow Mocks to be created outside of the specification - How to create Spock mocks outside of a specification class?
This seems to be still outstanding so am asking is that giving that i've got a complex and nested DI graph what is the most efficient way to 'inject' a mock representation deep in the graph?
Ideally, I have one bean definition set for normal deployment and another when running unit tests and it is this definition set being the applicable Mocks
e.g.
#Configuration
#Profile("deployment")
public class MyBeansForDeployment {
#Bean
public MyInterface myBean() {
return new MyConcreateImplmentation();
}
}
&&
#Configuration
#Profile("test")
public class MyBeansForUnitTests {
#Bean
public MyInterface myBean() {
return new MyMockImplementation();
}
}
Since Spock 1.1, you can create mocks outside of a specification class (detached mocks). One of the options is DetachedMockFactory. Take a look at the documentation or my answer to the question you linked.
You could try to implement a BeanPostProcessor that will replace the beans that you want with test doubles, such as shown below:
public class TestDoubleInjector implements BeanPostProcessor {
...
private static Map<String, Object[]> testDoubleBeanReplacements = new HashMap<>();
public void replaceBeanWithTestDouble(String beanName, Object testDouble, Class testDoubleType) {
testDoubleBeanReplacements.put(beanName, new Object[]{testDouble, testDoubleType});
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (testDoubleBeanReplacements.containsKey(beanName)) {
return testDoubleBeanReplacements.get(beanName)[TEST_DOUBLE_OBJ];
}
return bean;
}
In your test, setup your mocks like shown below before initializing the application context. Make sure to include the TestDoubleInjector as a bean in your test context.
TestDoubleInjector testDoubleInjector = new TestDoubleInjector()
testDoubleInjector.replaceBeanWithTestDouble('beanToReplace', mock(MyBean.class), MyBean.class)
It could be done using HotSwappableTargetSource
#WebAppConfiguration
#SpringApplicationConfiguration(TestApp)
#IntegrationTest('server.port:0')
class HelloSpec extends Specification {
#Autowired
#Qualifier('swappableHelloService')
HotSwappableTargetSource swappableHelloService
def "test mocked"() {
given: 'hello service is mocked'
def mockedHelloService = Mock(HelloService)
and:
swappableHelloService.swap(mockedHelloService)
when:
//hit endpoint
then:
//asserts
and: 'check interactions'
interaction {
1 * mockedHelloService.hello(postfix) >> { ""Mocked, $postfix"" as String }
}
where:
postfix | _
randomAlphabetic(10) | _
}
}
And this is TestApp (override the bean you want to mock with proxy)
class TestApp extends App {
//override hello service bean
#Bean(name = HelloService.HELLO_SERVICE_BEAN_NAME)
public ProxyFactoryBean helloService(#Qualifier("swappableHelloService") HotSwappableTargetSource targetSource) {
def proxyFactoryBean = new ProxyFactoryBean()
proxyFactoryBean.setTargetSource(targetSource)
proxyFactoryBean
}
#Bean
public HotSwappableTargetSource swappableHelloService() {
new HotSwappableTargetSource(new HelloService());
}
}
Have a look at this example https://github.com/sf-git/spock-spring