Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does java spring caching break reflection?

I'm using spring boot and integrated caching recently. Within my tests I use reflection a bit.

Here is an example:

@Service
public class MyService {

    private boolean fieldOfMyService = false;

    public void printFieldOfMyService() {
        System.out.println("fieldOfMyService:" + fieldOfMyService);
    }

    @Cacheable("noOpMethod")
    public void noOpMethod() {
    }

}

And this is the test:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { MyApplication.class })
public class MyServiceTest {

    @Autowired
    private MyService myService;

    @Test
    public void test() throws Exception {

        myService.printFieldOfMyService();

        boolean fieldOfMyService = (boolean) FieldUtils.readField(myService, "fieldOfMyService", true);

        System.out.println("fieldOfMyService via reflection before change:" + fieldOfMyService);

        FieldUtils.writeField(myService, "fieldOfMyService", true, true);

        boolean fieldOfMyServiceAfter = (boolean) FieldUtils.readField(myService, "fieldOfMyService", true);

        System.out.println("fieldOfMyService via reflection after change:" + fieldOfMyServiceAfter);

        myService.printFieldOfMyService();

    }

}

As you can see it's quite simple:

  • MyService has a private field fieldOfMyService
  • the test changes this from false to true via reflection

problem

  • everything works fine without caching.. this is the output:
fieldOfMyService:false
fieldOfMyService via reflection before change:false
fieldOfMyService via reflection after change:true
fieldOfMyService:true

Now I activate caching via:

  • @EnableCaching in spring
  • and then you get this:
fieldOfMyService:false
fieldOfMyService via reflection before change:false
fieldOfMyService via reflection after change:true
fieldOfMyService:false                      <<<<< !!!

Long story short:

  • when caching gets activated the service seems to be immune against changes made via reflection

The funny thing is, this only happens when the according service actually uses caching via at least one @Caching annotated method. In case the service does not have this like:

@Service
public class MyService {

    private boolean fieldOfMyService = false;

    public void printFieldOfMyService() {
        System.out.println("fieldOfMyService:" + fieldOfMyService);
    }

}

.. then it still works.

I guess this has something to do with the layers which get added when caching is activated. But... why? And is there a solution to it?

Thanks in advance for your help :-)

like image 829
daravetu Avatar asked Sep 10 '26 11:09

daravetu


1 Answers

The difference in behaviour is due to the proxying done by the Spring framework. When any special processing is required for a bean , in this case Caching , a proxy for the bean is created at runtime . There are two types of proxying techniques in Spring - JDK- and CGLIB-based proxies . Please read through the documentation link shared for more details.

In the example code shared , it is the CGLIB proxying that comes into play ( clue : MyService does not implement an interface) . The runtime subclass created by the CGLIB will have all the fields of the original class , but will remain null/default based on the data type ( here false). Also the method calls to the proxy is delegated to the original instance method.

Following changes to the code will give more details on how this works.

Change 1 : Printing the object.getClass() as well

@Service
public class MyService {

    private Boolean fieldOfMyService = false;

    public void printFieldOfMyService() {
        System.out.println("fieldOfMyService:" + this.getClass()+" : "+fieldOfMyService);
    }

    @Cacheable("noOpMethod")
    public void noOpMethod() {
    }

}

Test Class

@SpringBootTest(classes = { MyApplication.class })
public class MyServiceTest {

    @Autowired
    private MyService myService;

    @Test
    public void test() throws Exception {

        myService.printFieldOfMyService();

        boolean fieldOfMyService = (boolean) FieldUtils.readField(myService, "fieldOfMyService", true);

        System.out.println(
                "fieldOfMyService via reflection before change:" + myService.getClass() + " " + fieldOfMyService);

        FieldUtils.writeField(myService, "fieldOfMyService", true, true);

        boolean fieldOfMyServiceAfter = (boolean) FieldUtils.readField(myService, "fieldOfMyService", true);

        System.out.println(
                "fieldOfMyService via reflection after change:" + myService.getClass() + " " + fieldOfMyServiceAfter);

        myService.printFieldOfMyService();

    }

}

With @EnableCaching this code would print

fieldOfMyService:class rg.so.qn.MyService : false
fieldOfMyService via reflection before change:class rg.so.qn.MyService$$EnhancerBySpringCGLIB$$dfa75fca false
fieldOfMyService via reflection after change:class rg.so.qn.MyService$$EnhancerBySpringCGLIB$$dfa75fca true
fieldOfMyService:class rg.so.qn.MyService : false

Here the value updated through reflection is for the runtime subclass instance

Without @EnableCaching this code would print

fieldOfMyService:class rg.so.qn.MyService : false
fieldOfMyService via reflection before change:class rg.so.qn.MyService false
fieldOfMyService via reflection after change:class rg.so.qn.MyService true
fieldOfMyService:class rg.so.qn.MyService : true

Here the value updated through reflection is for the actual instance.

Change 2 : Modify the datatype of MyService.fieldOfMyService to Boolean

@Service
public class MyService {

    private Boolean fieldOfMyService = false;

    public void printFieldOfMyService() {
        System.out.println("fieldOfMyService:" + this.getClass()+" : "+fieldOfMyService);
    }

    @Cacheable("noOpMethod")
    public void noOpMethod() {
    }

}

With @EnableCaching this code would result in an NPE at MyServiceTest.test() , boolean fieldOfMyService = (boolean) FieldUtils.readField(myService, "fieldOfMyService", true); as the runtime proxy has the field initialized to null.

Without @EnableCaching this code would run as expect.

Hope this helps.

Note : @SpringBootTest is meta annotated with @ExtendWith and @RunWith can be avoided.

------------------------------- Update---------------------------------------

Thought of sharing the answer to this question as well "And is there a solution to it?"

Either use setter methods to update the state of the field

Or

If reflection is the only option , get the actual instance and use reflection to update the field.

Following code assumes that caching is enabled through @EnableCaching and the casting can work without any checks. Please make necessary modifications to make it foolproof.

@Test
public void test() throws Exception {

    myService.printFieldOfMyService();

    MyService actualInstance = (MyService)((Advised) myService).getTargetSource().getTarget();

    boolean fieldOfMyService = (boolean) FieldUtils.readField(actualInstance, "fieldOfMyService", true);

    System.out.println(
            "fieldOfMyService via reflection before change:" + myService.getClass() + " " + fieldOfMyService);

    FieldUtils.writeField(actualInstance, "fieldOfMyService", true, true);

    boolean fieldOfMyServiceAfter = (boolean) FieldUtils.readField(actualInstance, "fieldOfMyService", true);

    System.out.println(
            "fieldOfMyService via reflection after change:" + myService.getClass() + " " + fieldOfMyServiceAfter);

    myService.printFieldOfMyService();

}
like image 194
R.G Avatar answered Sep 12 '26 08:09

R.G



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!