mybatis / spring-boot-starter

MyBatis integration with Spring Boot
Apache License 2.0
4.13k stars 1.79k forks source link

auto-configuration and config location conflict. #39

Closed jcbvm closed 8 years ago

jcbvm commented 8 years ago

Currently I've an issue with setting up my project.

When I set the config property in my application.properties, MybatisAutoConfiguration still scans the project for possible mappers, causing all my interfaces in that package becoming a mapper file.

When debugging, I see the config check being done after the auto configuration is scanning my package. This seems like a bug to me.

eddumelendez commented 8 years ago

Hi @jcbvm are you talking about this?

screen shot 2016-03-30 at 8 00 11 pm

/cc @emacarron

jcbvm commented 8 years ago

@eddumelendez I'm talking about the AutoConfiguredMapperScannerRegistrar class in the class MybatisAutoConfiguration. The method registerBeanDefinitions is automatically scanning the base package. Not sure if this is intended, it seems so, because the documentation of the MapperScannerRegistrarNotFoundConfiguration class says:

"If it is not used, however, then this will bring in a bean registrar and automatically register components based on the same component-scanning path as Spring Boot itself."

My question is why it is by default scanning the package if the mappers are hard coded in the config file.

eddumelendez commented 8 years ago

It is working as spring-boot itself, if you are working with jpa in spring-boot yo don't need to put @Repository in your repositories classes, spring-boot does for you. The same happen with mybatis-spring-boot. The difference is, this project provided support for xml and java config. And as you can see there are some combinations that doesn't work as expected. I do not know if we can cover each case but, at the beginning, we add xml support to help people in the transition.

We can solve this removing the xml support :) I will take a look with some ideas in my mind but take into account that now xml and java config will be working together in the next version.

Thanks

jcbvm commented 8 years ago

Thanks, for now I will move out any interface from the spring boot application package, so it will not get picked up as a mapper file.

emacarron commented 8 years ago

Hi,

@jcbvm If you add you mapper files using the mybatis-config.xml file, MyBatis will know about the mappers but spring don't. You need that your mappers are scanned so they can be injected afterwards. When using Spring we expect that mappers become spring beans and scanning is the heart of spring boot :)

I am not familiar with spring boot JPA. I will take a look to have a better opinion about this topic, but it indeed looks like a bug that all the interfaces in a project are registered as mappers.

You can bypass this by adding a @MapperScan annotation that lets you specify lots of filters.

But honestly I do not like to mix the usage of @MapperScan with spring boot. I would prefer to specify everyting in the application.properties. The AutoConfiguredMapperScannerRegistrar can take lots of parameters to make a finer search like for example the base package. This can be achieved easily by porting code from @MapperScan (MapperScannerRegistrar).

If you don't mind lets keep ths issue open because it is worth having a discussion about how this feature works.

emacarron commented 8 years ago

Hi again,

Does anybody know how can we read the MyBatisProperties bean in AutoConfiguredMapperScannerRegistrar???

kazuki43zoo commented 8 years ago

@emacarron I don't know ... Can use the MapperScannerConfigurer instead of ?

emacarron commented 8 years ago

I don't think so. The registrar is the right bean to provide addional beans to a @Configuration object. But looks like when it is executed the MyBatisProperties bean has not yet been created. I tried to get the object out of the beanFactory but it is not yet there :(

BTW I have just had a fast look at spring-boot-starter-data-jpa. Looks like by default it searches for classes that implements the interface Repository or are annotated with @RepositoryDefinition so seems that it won't get all the interfaces in the project but just the right ones.

See https://github.com/spring-projects/spring-data-commons/blob/master/src/main/java/org/springframework/data/repository/config/RepositoryComponentProvider.java#L71

super.addIncludeFilter(new InterfaceTypeFilter(Repository.class));
super.addIncludeFilter(new AnnotationTypeFilter(RepositoryDefinition.class, true, true));

Unfortunately MyBatis mappers are not marked (by design) so a good idea would be for example to search with a pattern like basepackage + "/**/mapper" so mappers are expected to be inside a package called whatever/mappers. The problem now is that this will break existing code. But I wount care too much about that given the project has just born.

emacarron commented 8 years ago

Have a look at this, what do you think?

https://github.com/emacarron/spring-boot-starter/commit/e4a2c4b33222d5eb7ceb0027842dfd6944ac9953

kazuki43zoo commented 8 years ago

Can get property value via Environment instead of type-safe properties using EnvironmentAware ?

For example:

public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware,
        ImportBeanDefinitionRegistrar, ResourceLoaderAware , EnvironmentAware {
    // ...
    private Environment environment;
    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
    // ...
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
            BeanDefinitionRegistry registry) {

        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

        try {
            String[] mapperPackages = StringUtils.tokenizeToStringArray(environment.getProperty("mybatis.mapper-packages"),
                    ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            if(mapperPackages == null || mapperPackages.length == 0) {
                List<String> pkgs = AutoConfigurationPackages.get(this.beanFactory);
                for (String pkg : pkgs) {
                    log.debug("Found MyBatis auto-configuration package '" + pkg + "'");
                }
                if (this.resourceLoader != null) {
                    scanner.setResourceLoader(this.resourceLoader);
                }
                mapperPackages = pkgs.toArray(new String[pkgs.size()]);
            }
            scanner.registerFilters();
            scanner.doScan(mapperPackages);
        }
        catch (IllegalStateException ex) {
            log.debug("Could not determine auto-configuration "
                    + "package, automatic mapper scanning disabled.");
        }
    }
    // ...
}

What do you think ?

kazuki43zoo commented 8 years ago

Probably, above my idea is not good ... (sorry ...)

emacarron commented 8 years ago

Any idea is welcome @kazuki43zoo!! But I am afraid I already checked that and does not work.

Anyway, what about scanning only interfaces inside **/mapper packages. What do you think (@eddumelendez @jcbvm @kazuki43zoo) ?

kazuki43zoo commented 8 years ago

@emacarron , If possible i hope to support **/repository package. I use Mapper interface instead of Repository of DDD design(Spring's @Repository). What do you think ?

emacarron commented 8 years ago

Sounds good. I will probably make sense to search in these package names by default: **/mapper -> an opinionated mybatis tools should accept the "mybatis" main element **/repository -> in case you prefer the spring jargon, though this may collide if you are also using JPA **/persistence?? -> in case you copy & pasted from jpetstore-6? Should I better change the jpetstore-6?

Note that you yourself used the term "mapper" in the sample you make for gh-38: https://github.com/kazuki43zoo/mybatis-spring-boot-gh-38

This is because the term mapper rocks!!! doesn't it?? ;)

Already added to the sample 0a2c27d098c097f61a136414f1628eec3ef2e753

Note again that this will break existing code.

edit: I changed jpetstore-6 repo and the docs so the mappers package is now called "mapper"

kazuki43zoo commented 8 years ago

I prefer the repository package :smile: I think this solution will should provide since a next minor release (e.g. 1.1.0).

jcbvm commented 8 years ago

From my opinion, no package should be scanned by default. The base package should be defined in the config of spring boot. Which is in line with the legacy xml config property <mybatis:scan base-package="org.mybatis.spring.sample.mapper" />

emacarron commented 8 years ago

@jcbvm That makes sense when using raw spring but boot is supposed to use convention over configuration. In my opinion, an application should work by just adding a mapper and an autowired, with no further manual configuration.

Taking all interfaces as mappers will fulfill the objective but may cause too many side effects because you will have for sure interfaces that are not mappers.

OTOH I do agree in that we should be able to change the base package. Now that can only be done by adding a @MapperScan annotation to your SpringBoot main class. What is not so bad..

jcbvm commented 8 years ago

@emacarron That's truth, the question is how many configuration is allowed for using convention over configuration :P.

But I agree that it should be able to overwrite the defaults or even disable the whole scanning.

emacarron commented 8 years ago

I would not expect that someone that uses spring boot would not want to use scanning, because that is in fact the main feature of boot! :)

Right now scanning can only be disabled by adding one dummy Mapper and a @MapperScan configured to find it. Otherwise, if no MapperFactoryBean is found in the context the auto-scan will happen.

jcbvm commented 8 years ago

@emacarron Well for example if you have some complex queries and you want to build a Map with all kind of parameters for the query, you'll most likely want to create this Map in the Mapper implementation class in which you call the SqlSession yourself. In this case scanning is not necessary.

emacarron commented 8 years ago

Of course there are cases when you do not want your interface to be picked as a mapper. That is clear. But if you have a mapper (a mybatis interface with SQL annotations or an attached XML) then it should be always ok that it is autodiscovered.

The only situation I can imagine when auto-scanning does not work is when you have different datasources / sqlSessionFactories. That is a complex case. I would say it is good that simple cases work by default and complex cases require some deeper knowledge and finer tuning.

Anyway your comment reveals an interesting point. We should not scan in too many places so you can use other tecnologies without collision:

So to scan into a repository package we should invert the filters that jpa is using. That will be more or less:

scanner.addExcludeFilter(new InterfaceTypeFilter(Repository.class));
scanner.addExcludeFilter(new AnnotationTypeFilter(RepositoryDefinition.class, true, true));

The problem is that Repository objects may not be in the classpath so we need to add some annoying code to check its existance and add the optional dependency to pom.xml.

emacarron commented 8 years ago

@kazuki43zoo see commit f3619717cc59c18448bf005cef72d20d4f66fbdb with the exclusions. Not sure if this is a good approach. I am thinking that maybe we should not scan into "repository" to avoid the colision.... but please let me know your thoughts.

eddumelendez commented 8 years ago

I am not agree hardcoding packages to exclude because I have seen projects with other conventions and maybe we are going to open the door to support to other package exclusions and manages this as a issues, which will be crazy.

By default, we can exclude .mappers package but can be replaced by a property in MybatisProperties to support the customization.

One extreme approach could be remove the mybatis.config but I have seen several people requesting to support this property in combination with the other ones.

emacarron commented 8 years ago

Hi @eddumelendez !

More than exclusions we should decide which packages to include. Or... what sounds much better: which conventions are we going to use, following a "convention over configuration" strategy.

What I expect from spring boot is to have an aplication up and running with zero boilerplate. For that to happen, we need conventions that can be overriden only for special cases.

Right now, the mybatis boot starter module takes all the interfaces it founds in a project and registers them as mappers. Ok, that is not good but it is not so bad. The point is that we should be able to identify if an interface is a mapper or not.

The problem is that mappers are not annotated (the @Mapper annotation does not exist). We did this because we would like to build applications with no mybatis imports at all. So they way you select your mappers when using classic configuraiton like MapperScannerConfigurer or @MapperScan is by:

It would be great to be able to set up this same configuraion in the boot's application.properties file but unfortunately I do no know how to do it!! (see former messages). And anyway we still have @MapperScan for that (that is what you used in the 1st versions before Josh Long's contribution)

So, my proposal is to use this convention: mappers are supposed to be interfaces held in a **/mapper package. For any other configuration, use the @MapperScan annotation that lets you configure everything.

What do you think?

emacarron commented 8 years ago

Given that the original request is to disable scanning when there is a configuration set in the application.properties I will close this issue and start a new one about the scanner.

The reply for @jcbvm is that using a config file registers beans inside MyBatis but are not known to Spring, so in order to be able to inject mappers you need the mappers to be scanned also. Config and scanning can be used together.

There can be scenarios when a user does not want scanning at all for any reason, but we should handle that as an exception and give a solution that may be awful given that this is an edge case.

Lets follow the autoscan topic in #46

emacarron commented 8 years ago

@jcbvm Jacob, can you please check issue #46 ?