別のところで開発してもらった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以内の世界です。
こりゃいい感じですね!