SpringBootのCacheにCSVを入れてみた

 別のところで開発してもらったSpringBootのソースを引き取って機能追加の対応をしているのですが、毎日定時に更新されるCSVファイルをマスターデータとして使用するという要件が含まれてました。
 CSVをそのまま処理の都度読み込むのもイマイチだし、今回はSpringBootのアプリケーションなんで、SpringBootCacheを使ってみる事にしました。

今回の要件的にCacheとしてはConcurrentMapCacheでこと足りそうなので、実装前に下記サイトの住所CSV関東版を使って試してみます。
住所データのダウンロードサイト【住所.jp】

まず適当にエンティティを・・

import java.io.Serializable;

public class Address implements Serializable {

	private static final long serialVersionUID = 1L;

	/** 都道府県コード */ 
	private String prefid;
	
	/** 郵便番号 */
	private String zipCode;
	
	/** 都道府県名 */
	private String prefName;
	
	/** 住所 */
	private String addressName;
	
以下、GetterSetterは省略・・

キャッシュ設定のevictスケジュールについては、ここではテストなので1分でクリアし、クリアした事がわかるように出力してます。実際には日次CSVファイル更新処理が終わった後くらいに動作するようにします。

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

@Configuration
@EnableCaching
@EnableScheduling
public class CachingConfig {
	private final String cacheName = "AddressInfo";
	@Bean
    public CacheManager cacheManager() {
		ConcurrentMapCacheManager manager =  new ConcurrentMapCacheManager(cacheName);
		return manager;
    }
    @CacheEvict(allEntries = true, value = {cacheName})
    @Scheduled(fixedDelay = 60 * 1000 ,  initialDelay = 0)
    public void cacheEvict() {
    	System.out.println("evict!");
    }
}

で、サービスを作りますが、ここでCSVのデータを全部キャッシュに突っ込みます。CSV読み込みについてはこちらを参考にさせて頂きました。

事前にapplication.ymlに下記を書いておきます。

  jp:
    esoro:
      master:
        address: /var/lib/ap/data/kanto.csv
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class AddressService {
	
	protected static Logger logger = LogManager.getLogger();

	@Value("${jp.esoro.master.address}")
	String csvPath;

	@Cacheable("AddressInfo")
	public Map<String,Address> getAll() {
		BufferedReader in = null;
		Map<String,Address> list = new HashMap<String,Address>();
logger.info("readCSV");
		try{
			in = new BufferedReader(new InputStreamReader(new FileInputStream(new File(csvPath)),"MS932"));
			CSVParser parser = CSVFormat
			        .EXCEL                                        // ExcelのCSV形式を指定
			        .withIgnoreEmptyLines(true)                   // 空行を無視する
			        .withSkipHeaderRecord()                       // 最初の行をヘッダーとして読み飛ばす
			        .withIgnoreSurroundingSpaces(false)           // 値をtrimして取得する
			        .withRecordSeparator(System.getProperty("line.separator"))
			        .withDelimiter(',')
			        .withQuote('"')
			        .parse(in);

			// CSVのレコードを取得
		    List<CSVRecord> pList = parser.getRecords();

		    for(int i = 0 ; i < pList.size() ; i++ ){
		    	CSVRecord rec = pList.get(i);
		    	Address inf = new Address();
		    	inf.setPrefid(rec.get(1));
		    	inf.setZipCode(rec.get(4));
		    	inf.setPrefName(rec.get(7));
		    	inf.setAddressName(rec.get(9)+rec.get(11)+rec.get(13));
		    	list.put(rec.get(0), inf);
		    }
		} catch (Exception e) {
			logger.error("csv read error", e);
			 
		} finally{
			if (in != null){
				try {
					in.close();
				} catch (IOException e) {
				}
			}
		}
	    return list;
	}
}

依存しているものです

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-keyvalue</artifactId>
        </dependency>
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-csv -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-csv</artifactId>
            <version>1.5</version>
        </dependency>
    </dependencies>

最後にテストを書いて効果を確認します。

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import jp.esoro.cache.domain.model.address.AddressService;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = CacheApplication.class)
public class CacheTest {

	protected static Logger logger = LogManager.getLogger();

	@Autowired
	AddressService addressInfo;

	@Test
    public void testCache1() {
		
		for(int i = 0 ; i < 10;i++) {
			logger.info("get start");
			logger.info(addressInfo.getAll().get("299190600").getAddressName());
			logger.info(addressInfo.getAll().get("299190600").getAddressName());
			logger.info(addressInfo.getAll().get("161871000").getAddressName());
			try {
				Thread.sleep(1000*64);
			} catch (InterruptedException e) {
			}
		}
	}
}

テスト結果です。

evict!
2018-07-22 15:24:13.023  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : Started CacheTest in 2.497 seconds (JVM running for 3.93)
2018-07-22 15:24:13.201  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : get start
2018-07-22 15:24:13.215  INFO 8656 --- [           main] j.e.c.d.model.address.AddressService     : readCSV
2018-07-22 15:24:13.684  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : 安房郡鋸南町横根
2018-07-22 15:24:13.686  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : 安房郡鋸南町横根
2018-07-22 15:24:13.686  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : 文京区音羽
evict!
2018-07-22 15:25:17.686  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : get start
2018-07-22 15:25:17.687  INFO 8656 --- [           main] j.e.c.d.model.address.AddressService     : readCSV
2018-07-22 15:25:18.118  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : 安房郡鋸南町横根
2018-07-22 15:25:18.118  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : 安房郡鋸南町横根
2018-07-22 15:25:18.118  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : 文京区音羽
evict!
2018-07-22 15:26:22.118  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : get start
2018-07-22 15:26:22.119  INFO 8656 --- [           main] j.e.c.d.model.address.AddressService     : readCSV
2018-07-22 15:26:22.309  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : 安房郡鋸南町横根
2018-07-22 15:26:22.309  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : 安房郡鋸南町横根
2018-07-22 15:26:22.309  INFO 8656 --- [           main] jp.esoro.cache.CacheTest                 : 文京区音羽

CSVを読んだ場合500ms程度ですが、キャッシュが効いてると殆ど1ms以内の世界です。
こりゃいい感じですね!

カテゴリー: JavaSpring Boot   作成者: bokusui パーマリンク

bokusui について

ソフトウェアハウスでのPG・SEから始まり、10年近く勤めた金融系企業の社内SEを数年前にやめ、フリーランス時代を経たのち法人成りしました。システム開発の全工程をこじんまりとやり続けています。