After spending 2 days on this issue I really can't make any more progress on my own. I am working on a standard web application with Spring for dependency injection and the likes. I am also using Spring to cache several expensive methods I use a lot.
After I introduced Apache Shiro for the security layer, I was experiencing a strange issue where @Cacheable
methods in a certain service no longer got cached. To this point, I was able to strip the problem down to its core, but there's still a lot of code for you to look at - sorry for that...
First, I configure all relevant packages (all classes shown in the following are in one of those).
@Configuration
@ComponentScan(basePackages = {
"my.package.config",
"my.package.controllers",
"my.package.security",
"my.package.services",
})
public class AppConfiguration {
}
Here is the configuration file for caching.
@Configuration
@EnableCaching
public class CacheConfiguration {
@Bean(name = "cacheManager")
public SimpleCacheManager cacheManager() {
SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
simpleCacheManager.setCaches(Arrays.asList(
new ConcurrentMapCache("datetime")
));
return simpleCacheManager;
}
}
For my minimal example, I am using a very simple service that only returns the current timestamp. The Impl
class is as simple as you would imagine.
public interface DateService {
@Cacheable("datetime")
LocalDateTime getCurrent();
}
I inject this service into a controller.
@Controller
@RequestMapping("/v1/date")
public class DateController {
@Autowired
DateService dateService;
@RequestMapping(value = "/current", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<String> getCurrent() {
Subject s = SecurityUtils.getSubject();
s.login(new MyToken());
return new ResponseEntity<>(dateService.getCurrent().toString(), HttpStatus.OK);
}
}
The application is set up and started via Jetty, and everything works as expected so far. When calling <api-url>/v1/date/current
for the first time the current timestamp is returned, but afterwards one always receives the cached result.
Now, I introduce Shiro with yet another config file.
@Configuration
public class ShiroSecurityConfiguration {
@Bean
@Autowired
public DefaultSecurityManager securityManager(MyRealm realm) {
List<Realm> realms = new ArrayList<>();
// MyToken is a static stub for this example
realm.setAuthenticationTokenClass(MyToken.class);
realms.add(realm);
DefaultSecurityManager manager = new DefaultSecurityManager(realms);
SecurityUtils.setSecurityManager(manager);
return manager;
}
// other Shiro related beans that are - at least to me - irrelevant here
// EDIT 2: I figured out that the described problem only occurs with this bean
// (transitively depending on DateService) in the application
// the bean is required for annotations such as @RequiresAuthentication to work
@Bean
@Autowired
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
Finally, here comes the realm which also depends on my service.
@Component
public class MyRealm extends AuthenticatingRealm {
private static final String REALM_NAME = "MyRealm";
@Autowired
private DateService dateService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("User authenticated at "+dateService.getCurrent());
return new SimpleAuthenticationInfo("",token.getCredentials(),REALM_NAME);
}
}
With that, the caching is broken in my entire application. There is no error message, it just doesn't use the cache anymore. I was able to implement a workaround, but I am now seeking for a better solution and maybe also some advice to better understand the essence of my issue. So, here comes the workaround.
@Component
public class MyRealm extends AuthenticatingRealm {
private static final String REALM_NAME = "MyRealm";
private DateService dateService;
@Autowired
private ApplicationContext applicationContext;
private void wireManually() {
if (dateService == null) {
dateService = applicationContext.getBean(DateService.class);
}
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
wireManually();
System.out.println("User authenticated at "+dateService.getCurrent());
return new SimpleAuthenticationInfo("",token.getCredentials(),REALM_NAME);
}
}
Now it's back to working, and I was able to debug the reason for that. Shiro and hence MyRealm
gets initialized very early, even before the whole caching with my SimpleCacheManager
and all the related stuff (cacheInterceptor etc.) is loaded. Therefore, there is no proxy to wrap around the service when it gets initialized before the realm when using @Autowired
. With the workaround shown above, the service is not injected before everything is set up properly and the first request is being served, and then there is no problem.
Simply put, as soon as I make MyRealm
dependent on DateService
(annotating the last version of MyRealm
with @DependsOn("dateServiceImpl")
is enough to break the application) it gets initialized too early (i.e. before caching is set up).
So I would need to either postpone the initialization of MyRealm
, but I don't know how to do that. I tried @DependsOn("cacheManager")
, but that doesn't help as the other beans required for caching are loaded later nonetheless. Or - which is the same from another perspective - I could make sure the whole caching infrastructure (I am not enough of an expert to describe it in detail) is initialized earlier. Unfortunately, I also don't know how to do that...
Thanks in advance to everyone who made it to this point. Looking forward to any input, no matter if it's an idea to fix the code in a better way or an explanation why exactly Spring can't get this right on its own.
I finally figured out what the problem is and can at least explain its cause in more detail, even though my proposed solution is still a bit hacky.
Enabling the caching aspect in Spring introduces a org.springframework.cache.interceptor.CacheInterceptor
, which is essentially an org.aopalliance.aop.Advice
used by a org.springframework.cache.interceptor.BeanFactoryCacheOperationSourceAdvisor
that implements org.springframework.aop.Advisor
.
The org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor
I introduced for Shiro is another Advisor
which transitively depends on the DateService
via DefaultSecurityManager
and MyRealm
.
So I have two Advisor
s for two different aspects - Caching and Security - of which the one for security is initialized first. In fact, whenever I introduce any Advisor
dependent on DateService
- even if its only a dummy implementation as in the following example - the caching doesn't work anymore for the same reason as it was broken when adding Shiro. This causes the DateService
to be loaded before the caching aspect is ready, so it cannot be applied.
@Bean
@Autowired
public Advisor testAdvisor(DateService dateService) {
return new StaticMethodMatcherPointcutAdvisor() {
@Override
public boolean matches(Method method, Class<?> targetClass) {
return false;
}
};
}
Hence, the only proper fix for that is to change the order of aspect initialization. I am aware of the @Order(Ordered.LOWEST_PRECEDENCE)
respectively @Order(Ordered.HIGHEST_PRECEDENCE)
annotation for the case the multiple Advisor
s are applicable at a specific joinpoint, but this is not the case for me so this doesn't help. The order of initialization matters for other reasons.
Adding the following code in DateServiceImpl
actually solves the problem:
@Autowired
BeanFactoryCacheOperationSourceAdvisor waitForCachingAspect;
With that, the service always waits for the cache before it can be initialized even though this dependency is not used anywhere in the implementation. So now everything is working as it should because the dependency tree now includes Shiro --> DateService --> Cache
which makes the Shiro Advisor wait long enough.
It is still not as nice and clean as I would like it to be, but nevertheless, I think this explanation helps to understand the core of the problem and "How can I change the order in which Advisors are initialized in Spring" is a separate question I posted here.
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