View Javadoc
1   /*
2    * Copyright 2010-2024 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *    https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.mybatis.spring.mapper;
17  
18  import static org.assertj.core.api.Assertions.assertThat;
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertTrue;
21  import static org.junit.jupiter.api.Assertions.fail;
22  
23  import com.mockrunner.mock.jdbc.MockDataSource;
24  
25  import java.util.List;
26  import java.util.stream.Collectors;
27  import java.util.stream.Stream;
28  
29  import org.apache.ibatis.annotations.Mapper;
30  import org.apache.ibatis.session.ExecutorType;
31  import org.apache.ibatis.session.SqlSessionFactory;
32  import org.junit.jupiter.api.AfterEach;
33  import org.junit.jupiter.api.BeforeEach;
34  import org.junit.jupiter.api.Test;
35  import org.mybatis.spring.SqlSessionFactoryBean;
36  import org.mybatis.spring.SqlSessionTemplate;
37  import org.mybatis.spring.mapper.child.MapperChildInterface;
38  import org.mybatis.spring.type.DummyMapperFactoryBean;
39  import org.springframework.beans.factory.NoSuchBeanDefinitionException;
40  import org.springframework.beans.factory.config.BeanDefinition;
41  import org.springframework.beans.factory.config.ConstructorArgumentValues;
42  import org.springframework.beans.factory.config.RuntimeBeanReference;
43  import org.springframework.beans.factory.support.BeanDefinitionRegistry;
44  import org.springframework.beans.factory.support.GenericBeanDefinition;
45  import org.springframework.context.support.GenericApplicationContext;
46  import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
47  import org.springframework.context.support.SimpleThreadScope;
48  import org.springframework.mock.env.MockPropertySource;
49  import org.springframework.stereotype.Component;
50  
51  class MapperScannerConfigurerTest {
52    private GenericApplicationContext applicationContext;
53  
54    @BeforeEach
55    void setupContext() {
56      applicationContext = new GenericApplicationContext();
57  
58      // add the mapper scanner as a bean definition rather than explicitly setting a
59      // postProcessor on the context so initialization follows the same code path as reading from
60      // an XML config file
61      var definition = new GenericBeanDefinition();
62      definition.setBeanClass(MapperScannerConfigurer.class);
63      definition.getPropertyValues().add("basePackage", "org.mybatis.spring.mapper");
64      applicationContext.registerBeanDefinition("mapperScanner", definition);
65      applicationContext.getBeanFactory().registerScope("thread", new SimpleThreadScope());
66  
67      setupSqlSessionFactory("sqlSessionFactory");
68  
69      // assume support for autowiring fields is added by MapperScannerConfigurer via
70      // org.springframework.context.annotation.ClassPathBeanDefinitionScanner.includeAnnotationConfig
71    }
72  
73    private void startContext() {
74      applicationContext.refresh();
75      applicationContext.start();
76  
77      // this will throw an exception if the beans cannot be found
78      applicationContext.getBean("sqlSessionFactory");
79    }
80  
81    @AfterEach
82    void assertNoMapperClass() {
83      try {
84        // concrete classes should always be ignored by MapperScannerPostProcessor
85        assertBeanNotLoaded("mapperClass");
86  
87        // no method interfaces should be ignored too
88        assertBeanNotLoaded("package-info");
89        // assertBeanNotLoaded("annotatedMapperZeroMethods"); // as of 1.1.0 mappers with no methods are loaded
90      } finally {
91        applicationContext.close();
92      }
93    }
94  
95    @Test
96    void testInterfaceScan() {
97      startContext();
98  
99      var sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
100 
101     assertEquals(5, sqlSessionFactory.getConfiguration().getMapperRegistry().getMappers().size());
102 
103     // all interfaces with methods should be loaded
104     applicationContext.getBean("mapperInterface");
105     applicationContext.getBean("mapperSubinterface");
106     applicationContext.getBean("mapperChildInterface");
107     applicationContext.getBean("annotatedMapper");
108     applicationContext.getBean("scopedProxyMapper");
109     applicationContext.getBean("scopedTarget.scopedProxyMapper");
110 
111     assertThat(Stream.of(applicationContext.getBeanDefinitionNames()).filter(x -> x.startsWith("scopedTarget")))
112         .hasSize(1);
113     assertThat(applicationContext.getBeanDefinition("mapperInterface").getPropertyValues().get("mapperInterface"))
114         .isEqualTo(MapperInterface.class);
115     assertThat(applicationContext.getBeanDefinition("mapperSubinterface").getPropertyValues().get("mapperInterface"))
116         .isEqualTo(MapperSubinterface.class);
117     assertThat(applicationContext.getBeanDefinition("mapperChildInterface").getPropertyValues().get("mapperInterface"))
118         .isEqualTo(MapperChildInterface.class);
119     assertThat(applicationContext.getBeanDefinition("annotatedMapper").getPropertyValues().get("mapperInterface"))
120         .isEqualTo(AnnotatedMapper.class);
121     assertThat(applicationContext.getBeanDefinition("scopedTarget.scopedProxyMapper").getPropertyValues()
122         .get("mapperInterface")).isEqualTo(ScopedProxyMapper.class);
123   }
124 
125   @Test
126   void testNameGenerator() {
127     var definition = new GenericBeanDefinition();
128     definition.setBeanClass(BeanNameGenerator.class);
129     applicationContext.registerBeanDefinition("beanNameGenerator", definition);
130 
131     applicationContext.getBeanDefinition("mapperScanner").getPropertyValues().add("nameGenerator",
132         new RuntimeBeanReference("beanNameGenerator"));
133 
134     startContext();
135 
136     // only child inferfaces should be loaded and named with its class name
137     applicationContext.getBean(MapperInterface.class.getName());
138     applicationContext.getBean(MapperSubinterface.class.getName());
139     applicationContext.getBean(MapperChildInterface.class.getName());
140     applicationContext.getBean(AnnotatedMapper.class.getName());
141   }
142 
143   @Test
144   void testMarkerInterfaceScan() {
145     applicationContext.getBeanDefinition("mapperScanner").getPropertyValues().add("markerInterface",
146         MapperInterface.class);
147 
148     startContext();
149 
150     // only child inferfaces should be loaded
151     applicationContext.getBean("mapperSubinterface");
152     applicationContext.getBean("mapperChildInterface");
153 
154     assertBeanNotLoaded("mapperInterface");
155     assertBeanNotLoaded("annotatedMapper");
156   }
157 
158   @Test
159   void testAnnotationScan() {
160     applicationContext.getBeanDefinition("mapperScanner").getPropertyValues().add("annotationClass", Component.class);
161 
162     startContext();
163 
164     // only annotated mappers should be loaded
165     applicationContext.getBean("annotatedMapper");
166     applicationContext.getBean("mapperChildInterface");
167 
168     assertBeanNotLoaded("mapperInterface");
169     assertBeanNotLoaded("mapperSubinterface");
170   }
171 
172   @Test
173   void testMarkerInterfaceAndAnnotationScan() {
174     applicationContext.getBeanDefinition("mapperScanner").getPropertyValues().add("markerInterface",
175         MapperInterface.class);
176     applicationContext.getBeanDefinition("mapperScanner").getPropertyValues().add("annotationClass", Component.class);
177 
178     startContext();
179 
180     // everything should be loaded but the marker interface
181     applicationContext.getBean("annotatedMapper");
182     applicationContext.getBean("mapperSubinterface");
183     applicationContext.getBean("mapperChildInterface");
184 
185     assertBeanNotLoaded("mapperInterface");
186   }
187 
188   @Test
189   void testScopedProxyMapperScan() {
190     applicationContext.getBeanDefinition("mapperScanner").getPropertyValues().add("annotationClass", Mapper.class);
191 
192     startContext();
193     {
194       var definition = applicationContext.getBeanDefinition("scopedProxyMapper");
195       assertThat(definition.getBeanClassName()).isEqualTo("org.springframework.aop.scope.ScopedProxyFactoryBean");
196       assertThat(definition.getScope()).isEqualTo("");
197     }
198     {
199       var definition = applicationContext.getBeanDefinition("scopedTarget.scopedProxyMapper");
200       assertThat(definition.getBeanClassName()).isEqualTo("org.mybatis.spring.mapper.MapperFactoryBean");
201       assertThat(definition.getScope()).isEqualTo("thread");
202     }
203     {
204       var mapper = applicationContext.getBean(ScopedProxyMapper.class);
205       assertThat(mapper.test()).isEqualTo("test");
206     }
207     {
208       var mapper = applicationContext.getBean("scopedTarget.scopedProxyMapper", ScopedProxyMapper.class);
209       assertThat(mapper.test()).isEqualTo("test");
210     }
211     {
212       var mapper = applicationContext.getBean("scopedProxyMapper", ScopedProxyMapper.class);
213       assertThat(mapper.test()).isEqualTo("test");
214     }
215 
216     var sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
217     assertEquals(1, sqlSessionFactory.getConfiguration().getMapperRegistry().getMappers().size());
218   }
219 
220   @Test
221   void testScopedProxyMapperScanByDefault() {
222     applicationContext.getBeanDefinition("mapperScanner").getPropertyValues().add("defaultScope", "thread");
223 
224     startContext();
225 
226     List<String> scopedProxyTargetBeans = Stream.of(applicationContext.getBeanDefinitionNames())
227         .filter(x -> x.startsWith("scopedTarget")).collect(Collectors.toList());
228     assertThat(scopedProxyTargetBeans).hasSize(6).contains("scopedTarget.scopedProxyMapper",
229         "scopedTarget.annotatedMapper", "scopedTarget.annotatedMapperZeroMethods", "scopedTarget.mapperInterface",
230         "scopedTarget.mapperSubinterface", "scopedTarget.mapperChildInterface");
231 
232     for (String scopedProxyTargetBean : scopedProxyTargetBeans) {
233       {
234         var definition = applicationContext.getBeanDefinition(scopedProxyTargetBean);
235         assertThat(definition.getBeanClassName()).isEqualTo("org.mybatis.spring.mapper.MapperFactoryBean");
236         assertThat(definition.getScope()).isEqualTo("thread");
237       }
238       {
239         var definition = applicationContext.getBeanDefinition(scopedProxyTargetBean.substring(13));
240         assertThat(definition.getBeanClassName()).isEqualTo("org.springframework.aop.scope.ScopedProxyFactoryBean");
241         assertThat(definition.getScope()).isEqualTo("");
242       }
243     }
244     {
245       var mapper = applicationContext.getBean(ScopedProxyMapper.class);
246       assertThat(mapper.test()).isEqualTo("test");
247     }
248     {
249       var mapper = applicationContext.getBean(AnnotatedMapper.class);
250       assertThat(mapper.test()).isEqualTo("main");
251     }
252 
253     var sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
254     assertEquals(2, sqlSessionFactory.getConfiguration().getMapperRegistry().getMappers().size());
255   }
256 
257   @Test
258   void testScanWithExplicitSqlSessionFactory() {
259     setupSqlSessionFactory("sqlSessionFactory2");
260 
261     applicationContext.getBeanDefinition("mapperScanner").getPropertyValues().add("sqlSessionFactoryBeanName",
262         "sqlSessionFactory2");
263 
264     startContext();
265 
266     // all interfaces with methods should be loaded
267     applicationContext.getBean("mapperInterface");
268     applicationContext.getBean("mapperSubinterface");
269     applicationContext.getBean("mapperChildInterface");
270     applicationContext.getBean("annotatedMapper");
271   }
272 
273   @Test
274   void testScanWithExplicitSqlSessionTemplate() {
275     var definition = new GenericBeanDefinition();
276     definition.setBeanClass(SqlSessionTemplate.class);
277     var constructorArgs = new ConstructorArgumentValues();
278     constructorArgs.addGenericArgumentValue(new RuntimeBeanReference("sqlSessionFactory"));
279     definition.setConstructorArgumentValues(constructorArgs);
280     applicationContext.registerBeanDefinition("sqlSessionTemplate", definition);
281 
282     applicationContext.getBeanDefinition("mapperScanner").getPropertyValues().add("sqlSessionTemplateBeanName",
283         "sqlSessionTemplate");
284 
285     startContext();
286 
287     // all interfaces with methods should be loaded
288     applicationContext.getBean("mapperInterface");
289     applicationContext.getBean("mapperSubinterface");
290     applicationContext.getBean("mapperChildInterface");
291     applicationContext.getBean("annotatedMapper");
292   }
293 
294   @Test
295   void testScanWithExplicitSqlSessionFactoryViaPlaceholder() {
296     setupSqlSessionFactory("sqlSessionFactory2");
297 
298     // use a property placeholder for the session factory name
299     applicationContext.getBeanDefinition("mapperScanner").getPropertyValues().add("sqlSessionFactoryBeanName",
300         "${sqlSessionFactoryBeanNameProperty}");
301 
302     var props = new java.util.Properties();
303     props.put("sqlSessionFactoryBeanNameProperty", "sqlSessionFactory2");
304 
305     var propertyDefinition = new GenericBeanDefinition();
306     propertyDefinition.setBeanClass(PropertySourcesPlaceholderConfigurer.class);
307     propertyDefinition.getPropertyValues().add("properties", props);
308 
309     applicationContext.registerBeanDefinition("propertiesPlaceholder", propertyDefinition);
310 
311     startContext();
312 
313     // all interfaces with methods should be loaded
314     applicationContext.getBean("mapperInterface");
315     applicationContext.getBean("mapperSubinterface");
316     applicationContext.getBean("mapperChildInterface");
317     applicationContext.getBean("annotatedMapper");
318   }
319 
320   @Test
321   void testScanWithNameConflict() {
322     var definition = new GenericBeanDefinition();
323     definition.setBeanClass(Object.class);
324     applicationContext.registerBeanDefinition("mapperInterface", definition);
325 
326     startContext();
327 
328     assertThat(applicationContext.getBean("mapperInterface").getClass())
329         .as("scanner should not overwrite existing bean definition").isSameAs(Object.class);
330   }
331 
332   @Test
333   void testScanWithPropertyPlaceholders() {
334     var definition = (GenericBeanDefinition) applicationContext.getBeanDefinition("mapperScanner");
335 
336     // use a property placeholder for basePackage
337     definition.getPropertyValues().removePropertyValue("basePackage");
338     definition.getPropertyValues().add("basePackage", "${basePackageProperty}");
339     definition.getPropertyValues().add("processPropertyPlaceHolders", true);
340     // for lazy initialization
341     definition.getPropertyValues().add("lazyInitialization", "${mybatis.lazy-initialization:false}");
342 
343     // also use a property placeholder for an SqlSessionFactory property
344     // to make sure the configLocation was setup correctly and MapperScanner did not change
345     // regular property placeholder substitution
346     definition = (GenericBeanDefinition) applicationContext.getBeanDefinition("sqlSessionFactory");
347     definition.getPropertyValues().removePropertyValue("configLocation");
348     definition.getPropertyValues().add("configLocation", "${configLocationProperty}");
349 
350     var props = new java.util.Properties();
351     props.put("basePackageProperty", "org.mybatis.spring.mapper");
352     props.put("configLocationProperty", "classpath:org/mybatis/spring/mybatis-config.xml");
353     props.put("mybatis.lazy-initialization", "true");
354 
355     var propertyDefinition = new GenericBeanDefinition();
356     propertyDefinition.setBeanClass(PropertySourcesPlaceholderConfigurer.class);
357     propertyDefinition.getPropertyValues().add("properties", props);
358 
359     applicationContext.registerBeanDefinition("propertiesPlaceholder", propertyDefinition);
360 
361     startContext();
362 
363     var sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
364     System.out.println(sqlSessionFactory.getConfiguration().getMapperRegistry().getMappers());
365     assertEquals(1, sqlSessionFactory.getConfiguration().getMapperRegistry().getMappers().size());
366 
367     // all interfaces with methods should be loaded
368     applicationContext.getBean("mapperInterface");
369     applicationContext.getBean("mapperSubinterface");
370     applicationContext.getBean("mapperChildInterface");
371     applicationContext.getBean("annotatedMapper");
372 
373     assertEquals(5, sqlSessionFactory.getConfiguration().getMapperRegistry().getMappers().size());
374 
375     // make sure the configLocation was setup correctly
376     // mybatis-config.xml changes the executor from the default SIMPLE type
377     var sessionFactory = (SqlSessionFactory) applicationContext.getBean("sqlSessionFactory");
378     assertThat(sessionFactory.getConfiguration().getDefaultExecutorType()).isSameAs(ExecutorType.REUSE);
379   }
380 
381   @Test
382   void testScanWithMapperFactoryBeanClass() {
383     DummyMapperFactoryBean.clear();
384     applicationContext.getBeanDefinition("mapperScanner").getPropertyValues().add("mapperFactoryBeanClass",
385         DummyMapperFactoryBean.class);
386 
387     startContext();
388 
389     applicationContext.getBean("mapperInterface");
390     applicationContext.getBean("mapperSubinterface");
391     applicationContext.getBean("mapperChildInterface");
392     applicationContext.getBean("annotatedMapper");
393 
394     assertTrue(DummyMapperFactoryBean.getMapperCount() > 0);
395   }
396 
397   @Test
398   void testMapperBeanAttribute() {
399     startContext();
400 
401     assertThat(applicationContext.getBeanDefinition("annotatedMapper")
402         .getAttribute(ClassPathMapperScanner.FACTORY_BEAN_OBJECT_TYPE)).isEqualTo(AnnotatedMapper.class);
403   }
404 
405   @Test
406   void testMapperBeanOnConditionalProperties() {
407     var propertySources = applicationContext.getEnvironment().getPropertySources();
408     propertySources.addLast(new MockPropertySource().withProperty("mapper.condition", "true"));
409 
410     startContext();
411 
412     assertThat(applicationContext.getBeanDefinition("annotatedMapperOnPropertyCondition")
413         .getAttribute(ClassPathMapperScanner.FACTORY_BEAN_OBJECT_TYPE))
414             .isEqualTo(AnnotatedMapperOnPropertyCondition.class);
415   }
416 
417   private void setupSqlSessionFactory(String name) {
418     var definition = new GenericBeanDefinition();
419     definition.setBeanClass(SqlSessionFactoryBean.class);
420     definition.getPropertyValues().add("dataSource", new MockDataSource());
421     applicationContext.registerBeanDefinition(name, definition);
422   }
423 
424   private void assertBeanNotLoaded(String name) {
425     try {
426       applicationContext.getBean(name);
427       fail("Spring bean should not be defined for class " + name);
428     } catch (NoSuchBeanDefinitionException nsbde) {
429       // success
430     }
431   }
432 
433   public static class BeanNameGenerator implements org.springframework.beans.factory.support.BeanNameGenerator {
434 
435     @Override
436     public String generateBeanName(BeanDefinition beanDefinition, BeanDefinitionRegistry definitionRegistry) {
437       return beanDefinition.getBeanClassName();
438     }
439 
440   }
441 
442 }