Search code examples
javaspringspring-bootproxycglib

how to check if object instances using SCOPE_PROTOTYPE and ScopedProxyMode.TARGET_CLASS in Spring are different?


I have two beans PersonDAO and JdbcConnection with dependency on each other. The PersonDAO bean is a singleton bean using @Component. But the JdbcConnection is a prototype and because it is injected inside the PersonDAO I am using proxyMode = ScopedProxyMode.TARGET_CLASS to ensure that it is a different instance (not a singleton).

import com.github.felipegutierrez.explore.spring.basics.beans.JdbcConnection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class PersonDAO {
    @Autowired
    private JdbcConnection jdbcConnection;
    public PersonDAO(JdbcConnection jdbcConnection) {
        this.jdbcConnection = jdbcConnection;
    }
    public JdbcConnection getJdbcConnection() {
        return jdbcConnection;
    }
    public void setJdbcConnection(JdbcConnection jdbcConnection) {
        this.jdbcConnection = jdbcConnection;
    }
}

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class JdbcConnection {
    private static final Logger LOGGER = LoggerFactory.getLogger(JdbcConnection.class);
    public JdbcConnection() {
        // LOGGER.info("This is my JdbcConnection that is not a singleton bean.");
    }

}

When I print the object instance of them the PersonDAO is a singleton and the JdbcConnection is a different instance. But the hashcode of the JdbcConnection says that it is a singleton. Why is that?

ApplicationContext applicationContext = SpringApplication.run(ExploreSpringApplication.class, args);

PersonDAO personDAO01 = applicationContext.getBean(PersonDAO.class);
PersonDAO personDAO02 = applicationContext.getBean(PersonDAO.class);

LOGGER.info("DAO 01: {}, {}, JDBCConnection: {}, {}", personDAO01, personDAO01.hashCode(), personDAO01.getJdbcConnection().hashCode(), personDAO01.getJdbcConnection());
LOGGER.info("DAO 02: {}, {}, JDBCConnection: {}, {}", personDAO02, personDAO02.hashCode(), personDAO02.getJdbcConnection().hashCode(), personDAO02.getJdbcConnection());

output JDBCConnection of DAO01 and DAO02 has the same hashcode: 1596179075 but actually they are different instances: JdbcConnection@58a55449 and JdbcConnection@5949eba8:

2021-03-08 18:13:29.527  INFO 10329 --- [           main] c.g.f.e.spring.ExploreSpringApplication  : DAO 01: 1187972599, com.github.felipegutierrez.explore.spring.basics.dao.PersonDAO@46cf05f7
2021-03-08 18:13:29.527  INFO 10329 --- [           main] c.g.f.e.spring.ExploreSpringApplication  : DAO 01 JDBCConnection: 1596179075, com.github.felipegutierrez.explore.spring.basics.beans.JdbcConnection@58a55449
2021-03-08 18:13:29.529  INFO 10329 --- [           main] c.g.f.e.spring.ExploreSpringApplication  : DAO 02: 1187972599, com.github.felipegutierrez.explore.spring.basics.dao.PersonDAO@46cf05f7
2021-03-08 18:13:29.529  INFO 10329 --- [           main] c.g.f.e.spring.ExploreSpringApplication  : DAO 02 JDBCConnection: 1596179075, com.github.felipegutierrez.explore.spring.basics.beans.JdbcConnection@5949eba8

Solution

  • Spring uses CGLIB by default when creating a proxy for the target prototype object. It seems this is a limitation by CGLIB where the hashCode() and equals methods are always intercepted by special interceptors that do comparison in such a way that does not go through the target object.

    If you switch to JDK interface-based proxies, which requires that JdbcConnection implements an interface, you can declare hashCode() and equals() in the interface:

    @Component
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.INTERFACES)
    public class JdbcConnection implements IJdbcConnection {
    
        ...
    }
    
    interface IJdbcConnection {
    
        @Override
        boolean equals(Object obj);
    
        @Override
        int hashCode();
    }
    

    Spring's JdkDynamicAopProxy will then call the hashCode() and equals() on the target object.

    and use the @Qualifier("jdbcConnection") with the interface on the PersonDAO.

    @Component
    public class PersonDAO {
    
        @Autowired
        @Qualifier("jdbcConnection")
        private IJdbcConnection jdbcConnection;