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 fieldOfMyServicefalse to true via reflectionproblem
fieldOfMyService:false
fieldOfMyService via reflection before change:false
fieldOfMyService via reflection after change:true
fieldOfMyService:true
Now I activate caching via:
@EnableCaching in springfieldOfMyService:false
fieldOfMyService via reflection before change:false
fieldOfMyService via reflection after change:true
fieldOfMyService:false <<<<< !!!
Long story short:
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 :-)
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();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With