Skip to content

Instantly share code, notes, and snippets.

@jmondo
Last active May 12, 2020 19:11
Show Gist options
  • Save jmondo/b5a1e574165f0770e97d249e16b6adb6 to your computer and use it in GitHub Desktop.
Save jmondo/b5a1e574165f0770e97d249e16b6adb6 to your computer and use it in GitHub Desktop.

How do repos with magical name based lookups work?

To repro, put a debugger in org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepository(java.lang.Class<T>, org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments)

How Repos are initialized

Includes parsing the magical method name syntax

Repos are registered as beans on app boot, then their proxies are created lazily when you first autowire the repo.

The sample app

Spring boot app with this repo and application class.

// The one repo
@Repository
public interface BookRepository extends PagingAndSortingRepository<Book, UUID> {
    List<Book> findAllByTitle(String title);
}

// Application class
@Autowired
BookRepository bookRepository;
public static void main(String[] args) {
    SpringApplication.run(DatarestdemoApplication.class, args);
}

Boot time

  1. JpaRepositoriesAutoConfiguration for spring boot imports JpaRepositoriesRegistrar
  2. This adds the @EnableJpaRepositories annotation to the app config
  3. and it imports JpaRepositoriesRegistrar.class

    Note JpaRepositoriesRegistrar extends AbstractRepositoryConfigurationSourceSupport which implements ImportBeanDefinitionRegistrar which is "Interface to be implemented by types that register additional bean definitions when processing Configuration classes"

  4. on boot, spring comes around to do its @Configuration based bean creation (out of scope)
  5. JpaRepositoriesRegistrar is found and registerBeanDefinitions (in parent class) is called, which calls: org.springframework.data.repository.config.RepositoryConfigurationDelegate.registerRepositoriesIn

    JpaRepositoriesRegistrar looks like AbstractRepositoryConfigurationSourceSupport in the debugger, but that's an abstract class. If you check this you'll see it.

  6. the JpaRepositoriesRegistrar finds each repository and registers it with the root bean JpaRepositoryFactoryBean
  7. That bean has an after properties set hook (which bean registration picks up) org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#afterPropertiesSet
    1. Just sets up some stuff that we don’t really care about (afaik)

When you autowire the repo

  1. spring gets the autowire request

  2. Lazy inits a org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(java.lang.Class<T>, org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments)

  3. That sets up a AOP proxy programmatically

    ProxyFactory result = new ProxyFactory();
    result.setTarget(target);
    result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);

    note how there are 3 interfaces set up

  4. Adds a bunch of advices

    CrudMethodMetadataPopulatingMethodInterceptor
    PersistenceExceptionTranslationInterceptor
    TransactionInterceptor
    QueryExecutorMethodInterceptor
    ImplementationMethodExecutionInterceptor

    see below section for how this builds queries

  5. QueryExecutorMethodInterceptor constructor does its work:

    1. Constructor sets this.queries, by calling mapMethodsToQuery
    2. if you follow that all the way down, you end up in org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy.DeclaredQueryLookupStrategy.resolveQuery
    3. branch 1 tries to resolve by @Query annotation, then @Procedure annotation, then NamedQuery. It throws IllegalStateException because none exist.
    4. branch 2 (when branch 1 throws) is caught in here org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy.CreateIfNotFoundQueryLookupStrategy.createStrategy
    5. that builds a PartTreeJpaQuery which constructs a new org.springframework.data.repository.query.parser.PartTree
    6. this parses by a regex matcher, splitting subject "findAllBy", from predicate "Title".
    7. The org.springframework.data.repository.query.parser.PartTree.Predicate constructor splits the nodes of the predicate part. (if you had TitleAndAuthorFirstName, this would split Title and AuthorFirstName.)
    8. it first splits on "or", then passes each "or" group to the org.springframework.data.repository.query.parser.PartTree.OrPart
    9. that looks for ands and collects all the parts
    10. all of this is worked through org.springframework.data.jpa.repository.query.JpaQueryCreator.JpaQueryCreator and returned back to step 2 (resolveQuery)
  6. That uses type casting to get a proxy instance

    T repository = (T) result.getProxy(classLoader);

    in this case, the type is org.springframework.data.jpa.repository.support.SimpleJpaRepository

    Keep in mind, the repo itself is a proxy instance which includes an advice for QueryExecutorMethodInterceptor

When you call the repo method

Put a debugger in org.springframework.data.repository.core.support.RepositoryFactorySupport.QueryExecutorMethodInterceptor.invoke to repro this

Now go run the app and trigger one of the named queries like findAllByName()

  1. It gets the method from the proxy invocation, which goes through all the other proxies, eventually to the QueryExecutorMethodInterceptor
  2. org.springframework.data.repository.core.support.RepositoryFactorySupport.QueryExecutorMethodInterceptor.doInvoke pulls the query from the queries object that was set up above
  3. org.springframework.data.jpa.repository.query.JpaQueryExecution.execute
  4. you can trace that down to org.springframework.data.jpa.repository.query.PartTreeJpaQuery.QueryPreparer.createQuery(org.springframework.data.jpa.repository.query.JpaParametersParameterAccessor) ...
  5. org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute
  6. query.createQuery(accessor).getResultList(); 2. we jump through a bunch of methods called createQuery() 3. then we invoke method called createQuery on the entity manager aka SessionImpl (hibernate session))
    1. the type of the query is CriteriaQueryTypeQueryAdapter which is a JPA wrapper in hibernate
  7. getResultList() is hibernate code! https://www.objectdb.com/java/jpa/query/execute

Fin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment