本文介绍了在Shiro领域中自动装配时,具有可缓存方法的Spring服务在没有缓存的情况下被初始化的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在此问题上花了2天后,我真的无法再取得任何进步.我正在使用Spring在标准Web应用程序上进行依赖项注入之类的工作.我还使用Spring来缓存我经常使用的几种昂贵的方法.

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.

在安全层引入Apache Shiro之后,我遇到了一个奇怪的问题,即不再缓存某些服务中的@Cacheable方法.至此,我已将问题归结为核心问题,但是仍然有很多代码可供您查看-抱歉...

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;
    }
}

对于我的最小示例,我正在使用一个非常简单的服务,该服务仅返回当前时间戳. Impl类非常简单.

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);
    }
}

该应用程序是通过Jetty设置并启动的,到目前为止,一切都可以按预期进行.首次调用<api-url>/v1/date/current时,将返回当前时间戳,但此后总是会收到缓存的结果.

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.

现在,我将介绍Shiro和另一个配置文件.

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);
    }
}

现在,它可以恢复工作了,我能够调试原因了. Shiro以及MyRealm的初始化非常早,甚至在加载我的SimpleCacheManager和所有相关内容(cacheInterceptor等)的整个缓存之前也是如此.因此,在使用@Autowired时,在领域之前对服务进行初始化时,没有代理可以包装该服务.通过上面显示的解决方法,在正确设置所有内容并满足第一个请求之前,不会注入服务,因此没有问题.

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.

简而言之,一旦我使MyRealm依赖于DateService(用@DependsOn("dateServiceImpl")注释MyRealm的最新版本就足以破坏应用程序),它初始化得太早了(即在设置缓存之前)上).

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).

所以我需要推迟MyRealm的初始化,但是我不知道该怎么做.我尝试了@DependsOn("cacheManager"),但这无济于事,因为稍后仍会加载缓存所需的其他bean.或者-从另一个角度来看,这是相同的-我可以确保早先初始化整个缓存基础结构(我还没有足够的专家来详细描述它).不幸的是,我也不知道该怎么做...

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...

在此先感谢所有做到这一点的人.期待任何输入,无论是以更好的方式修复代码的主意,还是解释为何Spring本身都无法正确解决此问题.

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.

在Spring中启用缓存方面会引入org.springframework.cache.interceptor.CacheInterceptor,它实际上是实现org.springframework.aop.Advisororg.springframework.cache.interceptor.BeanFactoryCacheOperationSourceAdvisor所使用的org.aopalliance.aop.Advice.

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.

我为Shiro介绍的org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor是另一个Advisor,它通过DefaultSecurityManagerMyRealm可传递地依赖于DateService.

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.

所以对于两个不同的方面,我有两个Advisor:缓存和安全性-首先初始化安全性的两个.实际上,每当我引入依赖于DateService的任何Advisor时,即使它只是一个虚拟实现(如以下示例中所示),该缓存也不再起作用,其原因与添加Shiro时被破坏的原因相同.这会导致DateService在缓存方面准备就绪之前就已加载,因此无法应用.

So I have two Advisors 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;
        }
    };
}

因此,唯一的适当解决方法是更改​​方面初始化的顺序.对于多个Advisor适用于特定联接点的情况,我知道分别使用@Order(Ordered.LOWEST_PRECEDENCE)@Order(Ordered.LOWEST_PRECEDENCE)注释,但这对我而言不是这种情况,因此这无济于事.初始化的顺序因其他原因而重要.

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 Advisors 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.

DateServiceImpl中添加以下代码实际上可以解决该问题:

Adding the following code in DateServiceImpl actually solves the problem:

@Autowired
BeanFactoryCacheOperationSourceAdvisor waitForCachingAspect;

这样,即使在实现中的任何地方都没有使用此依赖项,服务也始终等待缓存才能初始化.所以现在一切都按预期进行,因为依赖项树现在包含Shiro --> DateService --> Cache,这使Shiro Advisor等待了足够长的时间.

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.

它仍然不如我所希望的那样干净整洁,但尽管如此,我认为这种解释有助于理解问题的核心,并且我如何更改在Spring中初始化Advisor的顺序"是一个单独的问题,我在这里发布.

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.

这篇关于在Shiro领域中自动装配时,具有可缓存方法的Spring服务在没有缓存的情况下被初始化的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

07-24 14:01