別のところで開発してもらったSpringBootのソースを引き取って機能追加の対応をしているのですが、毎日定時に更新されるCSVファイルをマスターデータとして使用するという要件が含まれてました。
CSVをそのまま処理の都度読み込むのもイマイチだし、今回はSpringBootのアプリケーションなんで、SpringBootCacheを使ってみる事にしました。
今回の要件的にCacheとしてはConcurrentMapCacheでこと足りそうなので、実装前に下記サイトの住所CSV関東版を使って試してみます。
住所データのダウンロードサイト【住所.jp】
まず適当にエンティティを・・
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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ファイル更新処理が終わった後くらいに動作するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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に下記を書いておきます。
1 2 3 4 |
jp: esoro: master: address: /var/lib/ap/data/kanto.csv |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
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; } } |
依存しているものです
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<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> |
最後にテストを書いて効果を確認します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
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) { } } } } |
テスト結果です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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以内の世界です。
こりゃいい感じですね!