Search code examples
javaspringspring-bootjunitspring-aop

Difference between @Autowired final setter and non-final setter in spring


Assuming:


    abstract class CommonService {

        protected VipMapper vipMapper;

        @Autowired
        public final void setVipMapper(VipMapper vipMapper) {
            this.vipMapper = vipMapper;
        }
    }

    @Service
    public class BookService extends CommonService {

        public int find() {
            return vipMapper.findVip(); // return 100
        }
    }

    @SpringBootTest
    class BookServiceTest {

        @Autowired
        private BookService bookService;

        @Test
        void find() {

            VipMapper v = new VipMapper() {
                @Override
                public int findVip() { // This method will not execute
                    return 10;
                }
            };

            bookService.setVipMapper(v);
            int find = bookService.find(); // find = 100 (not 10)
        }
    }

1. What is the reason I cannot inject VipMapper when setVipMapper method is final and I can inject when setVipMapper method is not final?

2. How can I inject VipMapper in runtime but still use @Autowired final setter?

Update

I'm using Spring + Mybatis

Source code: https://bitbucket.org/nguyentanh/stackoverflow

Using the above code, when run that test for findVipCustomerTop3, I get an error connection. But when to remove final in CommonService.java (or @Transactional in BookService.java), the test is success


Solution

    • You issue is not with autowiring. VipMapper get autowired correctly and you are trying to replace the mapper manually via bookService.setVipMapper(v); in your test. It does not replace the vipMapper you passed. To Check this behaviour, define a getter in your service to get the vipMapper and it will return the original vipMapper which was autowired by spring.

    • Just remember you are not working with an instance of your original BookService class, you are working with a sub class of BookService which is run time generated .

    • Your original question missed an important piece of info which is @Transactional annotation in your service. As soon as @Transactional annotation is there, Spring actually need to create a proxy. Now spring will choose JDK dynamic proxy or CGLIB proxy to create the proxy for your book service. Since your Service does not have an interface, JDK dynamic proxy choice is not possible so spring is left with CGLIB proxy.

    • CGLIB proxy has its limitations.

      With CGLIB, final methods cannot be advised, as they cannot be overridden in runtime-generated subclasses

    • Here is technique if you want to actually replace it. Add the following in your test class instead of the line bookService.setVipMapper(v);

        ((BookService)AopProxyUtils.getSingletonTarget(bookService))
                      .setVipMapper(v);
    
    • The above option is doing it hardcore way but good for understanding the concept. There is another option telling spring to create BookService in your test with a mock vipMapper autowired when it creates BookService.
         @SpringBootTest
         class BookServiceTest {
    
            @Autowired
            private BookService bookService;
            
            @MockBean
            private VipMapper vipMapper;
    
            @Test
            void find() {
                when(vipMapper.findVip()).thenReturn(10);
                bookService.setVipMapper(v);
                int find = bookService.find();
            }
        }
    

    Reference