web-dev-qa-db-ja.com

@ServiceクラスのSpringブートキャッシングが機能しない

@Serviceメソッドにいくつかの値を保存する際に問題が発生しました。私のコード:

@Service(value = "SettingsService")
public class SettingsService {
...

    public String getGlobalSettingsValue(Settings setting) {
        getTotalEhCacheSize();
        if(!setting.getGlobal()){
            throw new IllegalStateException(setting.name() + " is not global setting");
        }
        GlobalSettings globalSettings = globalSettingsRepository.findBySetting(setting);
        if(globalSettings != null)
            return globalSettings.getValue();
        else
            return getGlobalEnumValue(setting)
    }

@Cacheable(value = "noTimeCache", key = "#setting.name()")
    public String getGlobalEnumValue(Settings setting) {
        return Settings.valueOf(setting.name()).getDefaultValue();
    }

私のリポジトリクラス:

@Repository
public interface GlobalSettingsRepository extends CrudRepository<GlobalSettings, Settings> {

    @Cacheable(value = "noTimeCache", key = "#setting.name()", unless="#result == null")
    GlobalSettings findBySetting(Settings setting);

これは次のように機能するはずです。

  • データが存在する場合はDBから値を取得します。
  • 列挙型から値を保存しない場合。

ただし、DBまたは列挙型からのデータは保存されませんでした。

私のキャッシュ設定:

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public EhCacheCacheManager cacheManager(CacheManager cm) {
        return new EhCacheCacheManager(cm);
    }
    @Bean
    public EhCacheManagerFactoryBean ehcache() {
        EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();
        ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));

        return  ehCacheManagerFactoryBean;
    }
}

プロジェクトでrestメソッドでキャッシュが機能していることを確認する例がいくつかあります。

    @RequestMapping(value = "/system/status", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<?> systemStatus() {
        Object[] list = userPuzzleRepository.getAverageResponseByDateBetween(startDate, endDate);
...
}

public interface UserPuzzleRepository extends CrudRepository<UserPuzzle, Long> {
    @Cacheable(value = "averageTimeAnswer", key = "#startDate")
    @Query("select AVG(case when up.status='SUCCESS' OR up.status='FAILURE' OR up.status='TO_CHECK' then up.solvedTime else null end) from UserPuzzle up where up.solvedDate BETWEEN ?1 AND ?2")
    Object[] getAverageResponseByDateBetween(Timestamp startDate, Timestamp endDate);

そしてそれはうまくいきます。

私は何を間違っているのですか?

10
CSniper

SettingsServiceには2つのメソッドがあります。1つはキャッシュされます(getGlobalEnumValue(...))、もう1つはキャッシュされないが、もう1つのメソッドを呼び出します(getGlobalSettingsValue(...))。

ただし、Springキャッシュの抽象化が機能する方法は、クラスをプロキシすることです( Spring AOP を使用)。ただし、同じクラス内のメソッドを呼び出すと、プロキシされたロジックは呼び出されず、その下にある直接のビジネスロジックが呼び出されます。これは、同じBeanでメソッドを呼び出している場合、キャッシュが機能しないことを意味します。

したがって、getGlobalSettingsValue()を呼び出している場合、そのメソッドがgetGlobalEnumValue(...)を呼び出したときに、データが入力されたり、キャッシュが使用されたりすることはありません。


考えられる解決策は次のとおりです。

  1. プロキシを使用するときに同じクラスの別のメソッドを呼び出さない
  2. 他の方法もキャッシュする
  3. クラスをプロキシするのではなく、コンパイル時にコードをバイトコードに直接織り込むSpringAOPではなくAspectJを使用します。 @EnableCaching(mode = AdviceMode.ASPECTJ)を設定することでモードを切り替えることができます。ただし、 ロード時間ウィービングを設定する も行う必要があります。
  4. サービスをサービスに自動配線し、メソッドを直接呼び出すのではなく、そのサービスを使用します。サービスを自動配線することにより、プロキシをサービスに挿入します。
25
g00glen00b

問題は、キャッシュ可能なメソッドを呼び出す場所にあります。同じクラスから@Cacheableメソッドを呼び出す場合は、this参照から呼び出すだけです。つまり、Springのプロキシによってラップされないため、Springは呼び出しをキャッチして処理できません。

この問題を解決する方法の1つは、それ自体に@Autowiredサービスを提供し、この参照によってSpringが処理する必要があると予想されるメソッドを呼び出すことです。

@Service(value = "SettingsService")
public class SettingsService {
//...

    @Autowired
    private SettingsService settingsService;
//...
    public String getGlobalSettingsValue(Settings setting) {
       // ...
        return settingsSerive.getGlobalEnumValue(setting)
//-----------------------^Look Here
    }

    @Cacheable(value = "noTimeCache", key = "#setting.name()")
    public String getGlobalEnumValue(Settings setting) {
        return Settings.valueOf(setting.name()).getDefaultValue();
    }
}

しかし、そのような問題がある場合は、クラスの負担が大きすぎて、「単一クラス-単一責任」の原則に準拠していないことを意味します。より良い解決策は、@Cacheableを含むメソッドを専用クラスに移動することです。

9