- 投稿日:2020-01-24T21:12:50+09:00
Javaのequalsは結構気持ち悪い
導入
こんにちは、けちょんです。
Java書いてますか?
そんな私はここ数か月コーディングしてません。
焦ってJava goldの勉強を始めました(今日から)
その際気持ち悪かったことがあったので紹介します。なにが気持ち悪かったか
皆さん、文字列を比較する際、どう比較しますか?
素人プログラマの場合
String str1 = "test"; String str2 = "test"; if (str1 == str2) { System.out.print("same"); } else { System.out.print("diff"); }はい、だめですね。
こんなコード書いたら怒られます。はい、私です。
演算子である"=="はオブジェクトのメモリアドレスを比較します。異なるオブジェクトは基本的に異なるメモリアドレスを保持します。
今回比較したいのはオブジェクトの持つ文字列であるため、意図に反しています。が、この場合は"same"が出力されます。
String自体が気持ち悪いため、注意が必要です。
本題ではないし、詳しく知らないため省略します。一般プログラマの場合
String str1 = "1"; String str2 = "1"; if (str1.equals(str2)) { System.out.print("same"); } else { System.out.print("diff"); }普通ですね。よく見ます。
でも、皆さんObject.equalsの中身みたことありますか?中身は、、、
public boolean equals(Object obj) { return (this == obj); }"=="を使っとるやーーーん
"=="を使っていて怒られた私は大激怒です。
まとめ
文字列の比較はeqaulsも"=="も一緒????
いやいや
もちろんそんなことはありません。
equalsメソッドは、Stringクラスがオーバーライドしています。
以下がString.equalsの中身です。public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String aString = (String)anObject; if (coder() == aString.coder()) { return isLatin1() ? StringLatin1.equals(value, aString.value) : StringUTF16.equals(value, aString.value); } } return false; }これも"=="は使っているものの、メモリアドレスが異なる場合は別の比較を行っていますね。
まとめ
Object.equalsはただの"=="での比較
String.equalsは"=="で比較するものの、比較対象が文字列だった場合は特殊な動きをする(こちらの期待する動き)
- 投稿日:2020-01-24T20:00:48+09:00
Java14でdata class的な機能が追加されるらしい
JavaでフィールドとコンストラクタとGetter(場合によってはSetter,equals,toString)しか持たないクラスを書いたことがある人は多いと思います.そのようなクラスはデータクラスと呼ばれれています.
場合によっては似たようなクラスを大量に書かなければならないといけず,いくらIDEのメソッド生成機能を使っても煩わしいことこの上ないですよね.
lombokを使うと多少は軽減されるが,IDEの設定などをしないといけない場合も多く,面倒であることは変わりません.そんな中,この問題を解決するような機能がJava14で追加されるらしい.
Java14の新機能
Java14では
Record
なるものが導入されます.
https://blogs.oracle.com/javamagazine/records-come-to-java使い方は,データクラスにしたいクラスを
class
ではなくrecord
と宣言するらしい.使用例
使用前
FXOrderClassc.javapublic final class FXOrderClassic { private final int units; private final CurrencyPair pair; private final Side side; private final double price; private final LocalDateTime sentAt; private final int ttl; public FXOrderClassic(int units, CurrencyPair pair, Side side, double price, LocalDateTime sentAt, int ttl) { this.units = units; this.pair = pair; // CurrencyPair is a simple enum this.side = side; // Side is a simple enum this.price = price; this.sentAt = sentAt; this.ttl = ttl; } public int units() { return units; } public CurrencyPair pair() { return pair; } public Side side() { return side; } public double price() { return price; } public LocalDateTime sentAt() { return sentAt; } public int ttl() { return ttl; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; FXOrderClassic that = (FXOrderClassic) o; if (units != that.units) return false; if (Double.compare(that.price, price) != 0) return false; if (ttl != that.ttl) return false; if (pair != that.pair) return false; if (side != that.side) return false; return sentAt != null ? sentAt.equals(that.sentAt) : that.sentAt == null; } @Override public int hashCode() { int result; long temp; result = units; result = 31 * result + (pair != null ? pair.hashCode() : 0); result = 31 * result + (side != null ? side.hashCode() : 0); temp = Double.doubleToLongBits(price); result = 31 * result + (int) (temp ^ (temp >>> 32)); result = 31 * result + (sentAt != null ? sentAt.hashCode() : 0); result = 31 * result + ttl; return result; } @Override public String toString() { return "FXOrderClassic{" + "units=" + units + ", pair=" + pair + ", side=" + side + ", price=" + price + ", sentAt=" + sentAt + ", ttl=" + ttl + '}'; } }大量のGetterとコンストラクタ,Overrideメソッドが並んでいて見るだけでうんざりしますね.
これがJava14ではこうなります.FXOrder.javapublic record FXOrder(int units, CurrencyPair pair, Side side, double price, LocalDateTime sentAt, int ttl) {}
Kotlinみたい
とてもすっきりと宣言できるようになることが分かると思います.
ただし,Setterは用意されないようなので,ORマッパーなどを使う際は注意が必要です.まとめ
もうKotlinで良いと思う
Java14ではRecord
という強力な新機能が導入されるらしいです.
これでレガシーなコードも簡潔に書き換えることができそうです.
個人的にもこの機能はぜひ使いたいと思っていて,Java8からアップデートしようと考えています!
- 投稿日:2020-01-24T19:17:01+09:00
SendGrid Javaライブラリの、メール配信失敗時の実装を確認した話
前置き
JavaでWebアプリケーションを作成しています。
システムからメール配信を行うため、SendGridを利用しているのですが、なんらかの原因でメール配信が失敗した場合に、想定していた後処理がコケていたので備忘録として記載します。
すべては前任者の「エラー時にもResponseで結果を返してくるだろう」という思い込みから発生した実装でした。SendGridを使用してメール配信処理を作成
build.gradleにSendGrid Javaライブラリをプロジェクトにインストールしメール送信メソッドを作成。
SendGridが400番台のエラーを返してくる場合はこちらから送ったリクエストパラメータの不備やリクエスト超過のためリトライは行わず、ログを残してフロント側にエラー情報を返して処理を終了します。
500番台の場合はSendGridサーバにエラーが発生している状態のため、2〜3度リトライを行うように作成していました。
リトライ周りとか省略して(すみません)コードを記載します。dependencies { compile ('com.sendgrid:sendgrid-java:4.0.1') }import com.sendgrid.*; import java.io.IOException; public class Example { public static void main(String[] args) throws IOException { SendGrid sg = new SendGrid(System.getenv("SENDGRID_API_KEY")); try { Request request = new Request(); request.setMethod(Method.GET); request.setEndpoint("api_keys"); Response response = sg.api(request); if (response.getStatusCode() < 300) { // 200番台は正常送信として処理を続ける } else if (response.getStatusCode() < 500){ // 400番台エラーはリクエストパラメータが不正と判断して処理終了 // ログを残し、エラーメッセージをフロントへ返却 } else { // 500番台エラーはSendGridのサーバエラーのため何度かリトライ // ログは残しておく } } catch (IOException ex) { throw ex; } } }想定された処理が行われない問題
上記のように返ってきたresponse内のステータスコードを判断してその後の処理を振り分けるように作成していました
(前任者が)。
メール作成時のバリデーションチェック処理に不備があり、メールが送れなかった際に後処理が行われず、想定されたログも残っておらずでこの実装に不備があることが判明します。実装を確認する
com.sendgrid.SendGridクラスのapi()メソッドからたどって実際にどこでSendGridにリクエスト送信を行っているかを確認します。(3〜4ステップでたどり着けるので中略)
private Response executeApiCall(HttpRequestBase httpPost) throws IOException { try { CloseableHttpResponse serverResponse = httpClient.execute(httpPost); try { Response response = getResponse(serverResponse); if(response.getStatusCode() >= 300) { //throwing IOException here to not break API behavior. throw new IOException("Request returned status Code "+response.getStatusCode()+"Body:"+response.getBody()); } return response; } finally { serverResponse.close(); } } catch(ClientProtocolException e) { throw new IOException(e.getMessage()); } }com.sendgrid.ClientクラスにexecuteApiCall()メソッドがあり、この3行目でexecute()していること、そしてなにより6行目のif文で、IOExceptionがthrowされていることがわかります。
ステータスコードが300より大きいときも、responseにその値を詰めて返してくれていいのよ。。。SebdGridの返却値とその後
Caused by: java.io.IOException: Request returned status Code 400Body: { "errors": [ { "message": "Invalid replyTo email address", "field": "reply_to", "help": null } ] }SendGridは上記の様にエラーメッセージを返してくれるので、responseではなくcatchしたIOExceptionから後続処理を書くべきでした。
なお、ここ1〜2年SendGridでは大規模障害が発生していなさそうなことと、メールの使用率を鑑みてリトライ処理は消してさくっとエラーメッセージを表示するようにしました(バリデーションチェックは直したので400番台エラーは発生しなくなるはず!)。雑感
思い込みと諸々のテストの不備が重なったかなしい事件でしたが、使用ライブラリの返却値はしっかり確認して使おうねと学んだ出来事でした。
※あくまでJavaライブラリの実装なので、他言語のライブラリではエラー時にもresponseに詰めて返してくれるのかもしれません。
- 投稿日:2020-01-24T17:08:23+09:00
DBUnitの空文字列とか日付とか使いづらいので直して使う
1. はじめに
Java+RDBMSの開発プロジェクトで、単体テストツールにDBUnitを使用するケースは未だ(2020年)に多いと思います。
DBUnitはDBの初期データと実行後のアサーションデータをxls形式やCSV形式を利用できることで非常に便利かつ開発省力化に有効なツールですが、Excelのデータ形式やPOIの仕様の問題で若干使いづらい点があります。具体的には件名で挙げた通り、可変文字列型(VARCHAR型)の空文字列とNULLを分別できない点と、日付型(DATETIME、TIMESTAMP、DATE、TIME等)の値を正確に設定できない問題があります。
本稿では、この対策方法を記述します。
なお、本稿でのDbUnitのバージョンは2.5.4を使用しています。2. DBUnitはどうなっているのか?
2.1 セル情報の読み込み
DBUnitでは
XlsTable
クラスのgetValue(int row, String column)
メソッドで、Excelのセルの値を取得してDBに設定する値に変換しています。XlsTable#getValueから抜粋int type = cell.getCellType(); switch (type) { case HSSFCell.CELL_TYPE_NUMERIC: if (HSSFDateUtil.isCellDateFormatted(cell)) { return cell.getDateCellValue(); } return new BigDecimal(cell.getNumericCellValue()); case HSSFCell.CELL_TYPE_STRING: return cell.getStringCellValue(); case HSSFCell.CELL_TYPE_FORMULA: throw new DataTypeException("Formula not supported at row=" + row + ", column=" + column); case HSSFCell.CELL_TYPE_BLANK: return null; case HSSFCell.CELL_TYPE_BOOLEAN: return cell.getBooleanCellValue() ? Boolean.TRUE : Boolean.FALSE; case HSSFCell.CELL_TYPE_ERROR: throw new DataTypeException("Error at row=" + row + ", column=" + column); default: throw new DataTypeException("Unsupported type at row=" + row + ", column=" + column); }上記のコードからわかる通り、セルのデータ型が数値型で特定のフォーマットの場合に日付型としています。また、セルが空の場合にNULLとしています。
このため、DBに設定する日付型の値はExcelの日付データの精度によって差分が表示ます。たとえばExcel上では2020/1/24 10:00
と入れたものがDBのDATETIME型のカラムには2020/1/24 10:00:01
になることがあります。
また、セルの値が空の場合は一律nullになるため空文字列のデータを作ることができません。2.2 セル情報への出力
DBUnitには、DB(だけに限らないのですが)から読み取ったデータをExcelファイルに出力する機能も提供しています。
Excelへの出力はXlsDataSet
クラスのwrite(IDataSet dataSet, OutputStream out)
メソッドで行っています。
write
メソッドの内部でセルに設定する値の取得をしています。XlsDataSet#writeから抜粋// write table data for (int j = 0; j < table.getRowCount(); j++) { HSSFRow row = sheet.createRow(j + 1); for (int k = 0; k < columns.length; k++) { Column column = columns[k]; Object value = table.getValue(j, column.getColumnName()); if (value != null) { HSSFCell cell = row.createCell((short)k); cell.setEncoding(HSSFCell.ENCODING_UTF_16); cell.setCellValue(DataType.asString(value)); } } }上記のコードで
IDataSet
内のITable
に格納されている値を取得してセルに設定をしています。
値がnull
の場合はセルの生成自体をしないため、Excelの表示上は空のセルとなります。3. どう直すのか
上記で見たDBUnitのソースコードからDBUnitの処理の内容そのものの他に、データ型に対するExcelセル上の値の変換方法を利用者側で変更することができないこともわかります。
このため、DBUnitを都合よく使うためにXlsTable
とXlsDataSet
のソースコードを修正して独自のクラスを作り、そのクラスからDBUnitを使用することにします。3.1 セル情報の読み込み
XlsTable#getValue
メソッドで、セルのデータ型が文字列型の場合でも日付型のパータンにマッチする場合は日付型を設定するように変更します。
また、セル上にnull
と文字列を記載した場合はDBにNULL値を設定するようにします。XlsTable#getValueを修正int type = cell.getCellType(); switch (type) { case HSSFCell.CELL_TYPE_NUMERIC: if (HSSFDateUtil.isCellDateFormatted(cell)) { return cell.getDateCellValue(); } return new BigDecimal(cell.getNumericCellValue()); case HSSFCell.CELL_TYPE_STRING: /* 独自実装(ここから) */ String cellValue = cell.getRichStringCellValue().getString(); // セルの値が"null"の場合はNULLを設定 if ("null".equals(cellValue)) { return null; } // セルの値が"yyyy/MM/dd HH:mm:ss"形式の場合はDate型にパースして設定 if (Pattern.compile("\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2}").matcher(cellValue).matches()) { return new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(cellValue); } // 上記以外はセルの値をそのまま設定 return cellValue; /* 独自実装(ここまで) */ case HSSFCell.CELL_TYPE_FORMULA: throw new DataTypeException("Formula not supported at row=" + row + ", column=" + column); case HSSFCell.CELL_TYPE_BLANK: // セルが空の場合は空文字列を返却 return ""; case HSSFCell.CELL_TYPE_BOOLEAN: return cell.getBooleanCellValue() ? Boolean.TRUE : Boolean.FALSE; case HSSFCell.CELL_TYPE_ERROR: throw new DataTypeException("Error at row=" + row + ", column=" + column); default: throw new DataTypeException("Unsupported type at row=" + row + ", column=" + column); }3.2 セル情報への出力
JUnitの初期データ設定とDBデータの比較だけであればセル情報の読み込みだけを直せばよいのですが、DBデータのxls形式出力も使用すると試験データの作成を効率化できます。
そのため、セル情報読み込みの修正に合わせて、セル情報への出力処理も修正します。XlsDataSet#writeを修正// write table data for (int j = 0; j < table.getRowCount(); j++) { HSSFRow row = sheet.createRow(j + 1); for (int k = 0; k < columns.length; k++) { Column column = columns[k]; Object value = table.getValue(j, column.getColumnName()); /* 独自実装(ここから) */ if (null == value) { cell.setCellValue("null"); } else if (value instanceof java.sql.Timestamp) { cell.setCellValue(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(value)); } else { cell.setCellValue(DataType.asString(value)); } /* 独自実装(ここまで) */ } }値がnullの場合はセルの値に"null"文字列を設定するようにします。
データ型がTimestamp型の場合はyyyy/MM/dd HH:mm:ss
形式の文字列を設定するようにします。4. 独自クラスの実装
上記の方針で、実際に
XlsDataSet
とXlsTable
のソースコードを拝借して独自クラスを定義します。
修正箇所はJDKライブラリのみを使用しているので、DBUnitを使用できる環境であればコンパイル可能だと思います。
XlsDataSet
を修正したクラスとしてMyXlsDataSet
クラスを定義します。XlsTable
クラスはパッケージプライベートクラスとして定義されていて外部パッケージからはアクセスできないクラスとなっています。
ここでは、MyXlsTable
クラスをMyXlsDataSet
の内部クラスとして定義します。日付型についてはDATETIME型の他にDATE型やTIME型も考慮して、
Pattern
をキーDateFormat
を値とするHashMap
を定義して、Map
の各キーのPattern
を検査してマッチした場合はキーに紐づく値のDateFormat
でパースするようにしています。ここは、Map
ではなくてもPattern
とDateFormat
の組み合わせを格納しList
や配列でも構いません。このコードはあくまで試験用なので同期化を考慮していないので注意が必要です。
複数のスレッドからこのクラスを使ってExcelファイルを読み取る場合は、SimpleDateFormat
を都度作成するかThreadLocal
に格納するかの工夫が必要です。MyXlsDataSetpublic class MyXlsDataSet extends AbstractDataSet { private static final Logger logger = LoggerFactory.getLogger(MyXlsDataSet.class); /* 独自実装(ここから) */ private static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd"); private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss"); /* 独自実装(ここまで) */ private final ITable[] _tables; /** * Creates a new XlsDataSet object that loads the specified Excel document. */ public DateFormattedXlsDataSet(File file) throws IOException, DataSetException { this(new FileInputStream(file)); } /** * Creates a new XlsDataSet object that loads the specified Excel document. */ public DateFormattedXlsDataSet(InputStream in) throws IOException, DataSetException { HSSFWorkbook workbook = new HSSFWorkbook(in); _tables = new ITable[workbook.getNumberOfSheets()]; for (int i = 0; i < _tables.length; i++) { _tables[i] = new DateFormattedXlsTable(workbook.getSheetName(i), workbook.getSheetAt(i)); } } /** * Write the specified dataset to the specified Excel document. */ public static void write(IDataSet dataSet, OutputStream out) throws IOException, DataSetException { logger.debug("write(dataSet=" + dataSet + ", out=" + out + ") - start"); HSSFWorkbook workbook = new HSSFWorkbook(); int index = 0; ITableIterator iterator = dataSet.iterator(); while (iterator.next()) { // create the table i.e. sheet ITable table = iterator.getTable(); ITableMetaData metaData = table.getTableMetaData(); HSSFSheet sheet = workbook.createSheet(metaData.getTableName()); // write table metadata i.e. first row in sheet // workbook.setSheetName(index, metaData.getTableName(), HSSFWorkbook.ENCODING_UTF_16); workbook.setSheetName(index, metaData.getTableName()); HSSFRow headerRow = sheet.createRow(0); Column[] columns = metaData.getColumns(); for (int j = 0; j < columns.length; j++) { Column column = columns[j]; HSSFCell cell = headerRow.createCell((short) j); // cell.setEncoding(HSSFCell.ENCODING_UTF_16); cell.setCellValue(column.getColumnName()); } // write table data for (int j = 0; j < table.getRowCount(); j++) { HSSFRow row = sheet.createRow(j + 1); for (int k = 0; k < columns.length; k++) { Column column = columns[k]; Object value = table.getValue(j, column.getColumnName()); /* 独自実装(ここから) */ HSSFCell cell = row.createCell((short) k); if (null == value) { cell.setCellValue("null"); } else if (value instanceof java.sql.Timestamp) { cell.setCellValue(TIMESTAMP_FORMAT.format(value)); } else if (value instanceof java.sql.Date) { cell.setCellValue(DATE_FORMAT.format(value)); } else if (value instanceof Time) { cell.setCellValue(TIME_FORMAT.format(value)); } else { cell.setCellValue(DataType.asString(value)); } /* 独自実装(ここまで) */ // if (value != null) { // HSSFCell cell = row.createCell((short) k); // cell.setEncoding(HSSFCell.ENCODING_UTF_16); // cell.setCellValue(DataType.asString(value)); // } } } index++; } // write xls document workbook.write(out); out.flush(); } //////////////////////////////////////////////////////////////////////////// // AbstractDataSet class protected ITableIterator createIterator(boolean reversed) throws DataSetException { // logger.debug("createIterator(reversed=" + reversed + ") - start"); return new DefaultTableIterator(_tables, reversed); } private static class MyXlsTable extends AbstractTable { /* 独自実装(ここから) */ // UT用なので同期化は考慮しない private static final HashMap<Pattern, SimpleDateFormat> DATETIME_PATTERN_MAP = new HashMap<Pattern, SimpleDateFormat>(); static { DATETIME_PATTERN_MAP.put( Pattern.compile("\\d{4}/\\d{2}/\\d{2}"), new SimpleDateFormat("yyyy/MM/dd")); DATETIME_PATTERN_MAP.put( Pattern.compile("\\d{4}-\\d{2}-\\d{2}"), new SimpleDateFormat("yyyy-MM-dd")); DATETIME_PATTERN_MAP.put( Pattern.compile("\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2}"), new SimpleDateFormat("yyyy/MM/dd HH:mm:ss")); DATETIME_PATTERN_MAP.put( Pattern.compile("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}"), new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); } /* 独自実装(ここまで) */ // private static final Logger logger = LoggerFactory.getLogger(XlsTable.class); private final ITableMetaData _metaData; private final Sheet _sheet; private final DecimalFormatSymbols symbols = new DecimalFormatSymbols(); public DateFormattedXlsTable(String sheetName, Sheet sheet) throws DataSetException { int rowCount = sheet.getLastRowNum(); if (rowCount >= 0 && sheet.getRow(0) != null) { _metaData = createMetaData(sheetName, sheet.getRow(0)); } else { _metaData = new DefaultTableMetaData(sheetName, new Column[0]); } _sheet = sheet; // Needed for later "BigDecimal"/"Number" conversion symbols.setDecimalSeparator('.'); } static ITableMetaData createMetaData(String tableName, Row sampleRow) { logger.debug("createMetaData(tableName={}, sampleRow={}) - start", tableName, sampleRow); List columnList = new ArrayList(); for (int i = 0; ; i++) { Cell cell = sampleRow.getCell(i); if (cell == null) { break; } String columnName = cell.getRichStringCellValue().getString(); if (columnName != null) { columnName = columnName.trim(); } // Bugfix for issue ID 2818981 - if a cell has a formatting but no name also ignore it if (columnName.length() <= 0) { // logger.debug("The column name of column # {} is empty - will skip here assuming the // last column was reached", String.valueOf(i)); break; } Column column = new Column(columnName, DataType.UNKNOWN); columnList.add(column); } Column[] columns = (Column[]) columnList.toArray(new Column[0]); return new DefaultTableMetaData(tableName, columns); } //////////////////////////////////////////////////////////////////////////// // ITable interface public int getRowCount() { logger.debug("getRowCount() - start"); return _sheet.getLastRowNum(); } public ITableMetaData getTableMetaData() { logger.debug("getTableMetaData() - start"); return _metaData; } public Object getValue(int row, String column) throws DataSetException { if (logger.isDebugEnabled()) { logger.debug("getValue(row={}, columnName={}) - start", Integer.toString(row), column); } assertValidRowIndex(row); int columnIndex = getColumnIndex(column); Cell cell = _sheet.getRow(row + 1).getCell(columnIndex); if (cell == null) { return null; } int type = cell.getCellType(); switch (type) { case Cell.CELL_TYPE_NUMERIC: CellStyle style = cell.getCellStyle(); if (DateUtil.isCellDateFormatted(cell)) { return getDateValue(cell); } else if (XlsDataSetWriter.DATE_FORMAT_AS_NUMBER_DBUNIT.equals( style.getDataFormatString())) { // The special dbunit date format return getDateValueFromJavaNumber(cell); } else { return getNumericValue(cell); } case Cell.CELL_TYPE_STRING: /* 独自実装(ここから) */ String cellValue = cell.getRichStringCellValue().getString(); if ("null".equals(cellValue)) { return null; } Set<Pattern> patternSet = DATETIME_PATTERN_MAP.keySet(); for (Pattern pattern : patternSet) { if (pattern.matcher(cellValue).matches()) { SimpleDateFormat format = DATETIME_PATTERN_MAP.get(pattern); try { return format.parse(cellValue); } catch (ParseException e) { continue; } } } return cellValue; /* 独自実装(ここまで) */ case Cell.CELL_TYPE_FORMULA: throw new DataTypeException("Formula not supported at row=" + row + ", column=" + column); case Cell.CELL_TYPE_BLANK: return ""; // 独自実装 case Cell.CELL_TYPE_BOOLEAN: return cell.getBooleanCellValue() ? Boolean.TRUE : Boolean.FALSE; case Cell.CELL_TYPE_ERROR: throw new DataTypeException("Error at row=" + row + ", column=" + column); default: throw new DataTypeException("Unsupported type at row=" + row + ", column=" + column); } } protected Object getDateValueFromJavaNumber(Cell cell) { logger.debug("getDateValueFromJavaNumber(cell={}) - start", cell); double numericValue = cell.getNumericCellValue(); BigDecimal numericValueBd = new BigDecimal(String.valueOf(numericValue)); numericValueBd = stripTrailingZeros(numericValueBd); return new Long(numericValueBd.longValue()); } protected Object getDateValue(Cell cell) { logger.debug("getDateValue(cell={}) - start", cell); double numericValue = cell.getNumericCellValue(); Date date = DateUtil.getJavaDate(numericValue); return new Long(date.getTime()); } /** * Removes all trailing zeros from the end of the given BigDecimal value up to the decimal * point. * * @param value The value to be stripped * @return The value without trailing zeros */ private BigDecimal stripTrailingZeros(BigDecimal value) { if (value.scale() <= 0) { return value; } String valueAsString = String.valueOf(value); int idx = valueAsString.indexOf("."); if (idx == -1) { return value; } for (int i = valueAsString.length() - 1; i > idx; i--) { if (valueAsString.charAt(i) == '0') { valueAsString = valueAsString.substring(0, i); } else if (valueAsString.charAt(i) == '.') { valueAsString = valueAsString.substring(0, i); // Stop when decimal point is reached break; } else { break; } } BigDecimal result = new BigDecimal(valueAsString); return result; } protected BigDecimal getNumericValue(Cell cell) { logger.debug("getNumericValue(cell={}) - start", cell); String formatString = cell.getCellStyle().getDataFormatString(); String resultString = null; double cellValue = cell.getNumericCellValue(); if ((formatString != null)) { if (!formatString.equals("General") && !formatString.equals("@")) { logger.debug("formatString={}", formatString); DecimalFormat nf = new DecimalFormat(formatString, symbols); resultString = nf.format(cellValue); } } BigDecimal result; if (resultString != null) { try { result = new BigDecimal(resultString); } catch (NumberFormatException e) { logger.debug("Exception occurred while trying create a BigDecimal. value={}", resultString); // Probably was not a BigDecimal format retrieved from the excel. Some // date formats are not yet recognized by HSSF as DateFormats so that // we could get here. result = toBigDecimal(cellValue); } } else { result = toBigDecimal(cellValue); } return result; } /** * @param cellValue * @return * @since 2.4.6 */ private BigDecimal toBigDecimal(double cellValue) { String resultString = String.valueOf(cellValue); // To ensure that intergral numbers do not have decimal point and trailing zero // (to restore backward compatibility and provide a string representation consistent with // Excel) if (resultString.endsWith(".0")) { resultString = resultString.substring(0, resultString.length() - 2); } BigDecimal result = new BigDecimal(resultString); return result; } public String toString() { StringBuilder sb = new StringBuilder(); sb.append(getClass().getName()).append("["); sb.append("_metaData=").append(this._metaData == null ? "null" : this._metaData.toString()); sb.append(", _sheet=").append(this._sheet == null ? "null" : "" + this._sheet); sb.append(", symbols=").append(this.symbols == null ? "null" : "" + this.symbols); sb.append("]"); return sb.toString(); } } }引用したソースコードが2020年1月24日現在で最新の2.6.0になっていましたが、検証に使用したDBUnitのバージョンは2.5.4であるため、Excelのエンコードに関する設定をコメントアウトしています。
2.6.0以上を使用する場合は下記の行のコメントを解除してください。// workbook.setSheetName(index, metaData.getTableName(), HSSFWorkbook.ENCODING_UTF_16);// cell.setEncoding(HSSFCell.ENCODING_UTF_16);5. 独自クラスを使う
上記の独自クラスのコンパイルが成功したら、使い方は
XlsDataSet
と全く同じです。
ただし、DBUnitは標準で空白のセルの出力を許していないため、取得したDatabaseConnection
に対してDatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS
をtrueにする処理を入れた方が良いです。ExcelデータをDBに設定DatabaseConnection connection = new DatabaseConnection(sqlConnection, schemaName); connection.getConfig().setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true); IDataSet xlsDataSet = new MyXlsDataset(new File(xlsFilePath)); DatabaseOperation.CLEAN_INSERT.execute(connection, compositDataSet);ExcelデータとDBを比較DatabaseConnection connection = new DatabaseConnection(sqlConnection, schemaName); connection.getConfig().setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true); IDataSet expected= new MyXlsDataset(new File(expectedXlsFilePath)); QueryDataSet actual = new QueryDataSet(connection); Assert.assertEquals(expected, actual);DBデータをExcelファイルに出力DatabaseConnection connection = new DatabaseConnection(sqlConnection, schemaName); connection.getConfig().setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true); QueryDataSet dbDataSet= new QueryDataSet(connection); FileOutputStream fileOutputStream = new FileOutputStream(outputFilePath); MyXlsDataset.write(dbDataSet, fileOutputStream);gradleを使用しているプロジェクトでは、gradleタスクとしてDBUnitを使ってDBのエクスポートやインポートをしたくなるかもしれません。もちろんそれは可能ですが、その場合は上記の
MyXlsDataSet
クラスはjarライブラリにする必要があります。6. さいごに
DBUnitはLGPLライセンスを持っています。
作成したプログラムのテストコードを含めて配布する場合など上記のコードを実装コードに含める場合は、LGPLライセンスに従う必要があるので注意をしてください。
- 投稿日:2020-01-24T16:45:18+09:00
libGDXでandroidアプリのチューニングをした話
はじめに
もう9年前。iOSアプリが日本ランキングの1位になった。
iOSソリティアV
さりげなく自慢調子に乗ってandroidアプリも作った。nativeで作った。
nativeで当時android2.xなので、SurfaceViewを使った。
androidソリティアVアニメがいまいちだったけど、ちゃんとできた。
フレームレートがちょっと低かった。android/iOS共に現在10周年記念中。
数年前、チューニングしたいな、と思った。
iOSとソース一本化
したいけど、一本化するにはやることが多すぎる・・・。うーん、GLSerfaceViewとかか。めんどくさそうだぞ。しかもよくなるか保証がない。
そんなとき、こんな記事が
Android StudioでlibGDX入門ああlibGDX・・・web化しようとして、前にやったなあ。GWTがいまいちで途中で止めたんだった。
一部の画面のみlibGDXとか、できるのか。そんなわけで一部の機能のみlibGDX、問題ない機能はそのままnative。な構成のandroidアプリとしてチューニングしてみることになったのであった。
結果
良かった点
- ちゃんとチューニングになった。開発も結構サクサクいけた。
- 前に捨てたlibGDXコードもちゃんと再利用できた。
- Javaソース追えるので、いろいろ助かった。
- android nativeのレイアウトをlibGDX画面の上に出したりもできた。ほんとに最低限のみ、libGDX化で行けた。楽だった。
- 使い慣れたテクスチャパッカーも使える。
悪かった点
- 日本語の突っ込んだ情報が無い・・・
最後に
結構良かった。androidアプリとして、間違った選択ではなかった。
- 投稿日:2020-01-24T15:55:12+09:00
Java備忘 LocalDateを使ってみた
はじめに
Javaで○日以上古いファイルを検出するというプログラムを書く機会があったので、備忘として残しておく。
あくまで一つの例として。LocalDateを使う
Dateでも可能だが、より新しいLocalDateを使ってみる。
Long lastModified = targetFile.lastModified(); LocalDate lastDate = Instant.ofEpochMilli(lastModified).atZone(ZoneId.systemDefault()).toLocalDate();これでファイルクラスで宣言した対象のファイルから最終更新日を取得する。
エポック時間をLocalDate型に変換しているイメージ。LocalDate daysBefore = LocalDate.now().minusDays(5);LocalDateクラスのnow()で本日の日付を取得する。
今回は5日前の日付を対象にするとして、minusDays(5)で取得することができる。if (lastModified.isBefore(base) || lastModified.isEqual(base)) if (lastModified.compareTo(base) <= 0) if (lastModified.until(LocalDate.now(), ChronoUnit.DAYS) >= 5)あとはこんな感じのif文を書いてあげれば指定した日付以前のファイルを炙り出せるはず。
注意したいのはisBefore()だけでは5日より前しか取れないこと。
isEqual()で5日前も取れるようにしてあげる。追記
@swordoneさんからコメントをいただき、
if文の条件式を追加しました。まとめ
ここにFileクラスをピピーっとぶっ込んでやるといい感じなる。
ご指摘等ございましたらコメントお願いします。
- 投稿日:2020-01-24T15:53:40+09:00
Eclipse JavaからSQLファイルに飛ぶ(ジャンプ)方法
- 投稿日:2020-01-24T15:53:40+09:00
Eclipse JavaからSQLファイルに飛ぶ方法
- 投稿日:2020-01-24T11:03:04+09:00
Azure EventhubsにSASトークンを使う方法(Java)
はじめに
こんにちは、とがりと申します。
SREをしています。なぜ書いたか
Azure Eventhubsを使っていて調べても全然ドキュメント出てこなく、詰まったから。
本記事はJavaの環境が既に用意されている人を対象にしております。(maven, gradle, etc.)本題
やりたいこと
アンドロイドアプリからAzure Eventhubsにデータを送信
Shared Access Signature 用いてEventhubsにアクセスしたい。1
しかし、SASを持っていればEventhubsに対して半永久的にアクセスすることができてしまうため
クライアントサイドに配置するのはセキュリティの観点で不安である。
何かいい方法がないかと探していたところSasからTokenを生成できるという情報を得た。
なのでSasTokenを用いてEventhubsにデータを送信する。
詰まった部分もあったので備忘録として記す。やってみる
- SasTokenを作成する
- SasTokenを用いてEventhubsにメッセージを送信する
- ちゃんとメッセージが届いているかチェックする
1. SasTokenを作成する
Azure ポータル
→Eventhubs 名前空間
→Eventhubs インスタンス
→共有アクセスポリシー
→共有アクセスポリシー名
→主キー&接続文字列–主キー
を使います。
接続文字列–主キーの
Endpoint=sb://(Eventhubs名).servicebus.windows.net/;SharedAccessKeyName=.......;EntityPath=(エントリーポイント)
これを変形して以下の[RESOURCEURI]に用います使う変数[RESOURCEURI]=sb://(Eventhubs名).servicebus.windows.net/(エントリーポイント) [KEYNAME]=共有アクセスポリシー名 [KEY]=共有アクセスポリシー名の主キーSASToken.javaimport java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; public class SASToken { public static void main(String[] args) { long epoch = System.currentTimeMillis() / 1000L; int week = 60 * 60 * 24 * 7; String resourceUri ="[RESOURCEURI]"; String keyName = "[KEYNAME]"; String key = "[KEY]"; String expiry = Long.toString(epoch + week); String sasToken = null; try { String stringToSign = URLEncoder.encode(resourceUri, "UTF-8") + "\n" + expiry; String signature = getHMAC256(key, stringToSign); sasToken = "SharedAccessSignature sr=" + URLEncoder.encode(resourceUri, "UTF-8") + "&sig=" + URLEncoder.encode(signature, "UTF-8") + "&se=" + expiry + "&skn=" + keyName; } catch (UnsupportedEncodingException e) { e.printStackTrace(); } System.out.println(sasToken); } // hash値を返す関数 public static String getHMAC256 (String key, String input){ Mac sha256_HMAC = null; String hash = null; try { sha256_HMAC = Mac.getInstance("HmacSHA256"); SecretKeySpec secret_key = new SecretKeySpec(key.getBytes(), "HmacSHA256"); sha256_HMAC.init(secret_key); Base64.Encoder encoder = Base64.getEncoder(); hash = new String(encoder.encode(sha256_HMAC.doFinal(input.getBytes("UTF-8")))); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (IllegalStateException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return hash; } }これを実行するとsasTokenが得られます。
2. SasTokenを用いてEventhubsにメッセージを送信する
使う変数[1で得たSasToken]=上記の結果 [NAMESPACENAME]=イベントハブの名前空間 [EVENTHUBNAME]=イベントハブインスタンスの名前SendToEventhubs.javaimport com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.microsoft.azure.eventhubs.ConnectionStringBuilder; import com.microsoft.azure.eventhubs.EventData; import com.microsoft.azure.eventhubs.EventHubClient; import com.microsoft.azure.eventhubs.EventHubException; import java.io.IOException; import java.nio.charset.Charset; import java.time.Instant; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; public class SendToEventhubs { public static void main(String[] args) throws EventHubException, ExecutionException, InterruptedException, IOException { String sas = "[1で得たSasToken]"; // 認証情報をセット final ConnectionStringBuilder connStr = new ConnectionStringBuilder() .setNamespaceName("[NAMESPACENAME]") .setEventHubName("[EVENTHUBNAME]") .setSharedAccessSignature(sas); final Gson gson = new GsonBuilder().create(); final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(4); // イベントハブクライアントを記述 final EventHubClient ehClient = EventHubClient.createSync(connStr.toString(), executorService); try { for (int i = 0; i < 1; i++) { String payload = "Hello, Eventhubs !! (ここにメッセージいれる)"; byte[] payloadBytes = gson.toJson(payload).getBytes(Charset.defaultCharset()); EventData sendEvent = EventData.create(payloadBytes); System.out.println(sendEvent); // イベントハブに送信 ehClient.sendSync(sendEvent); } System.out.println(Instant.now() + ": Send Complete..."); System.out.println("Press Enter to stop."); System.in.read(); } finally { ehClient.closeSync(); executorService.shutdown(); } } }これでイベントの送信が完了しました。ただこれでは確認できないので受信する方法も乗せておきます。
3. ちゃんとメッセージが届いているかチェックする
使う変数String consumerGroupName = "$Default"; String namespaceName = "[NAMESPACENAME]"; String eventHubName = "[EVENTHUBNAME]"; String sasKeyName = "[KEYNAME]"; String sasKey = "[KEY]"; String storageConnectionString = "[STORAGECONNECTION]"; //ポータル→ストレージアカウント→アクセスキー String storageContainerName = "[STORAGECONTAINERNAME]"; //ポータル→ストレージアカウント→コンテナー String hostNamePrefix = "";EventProcessorSample.javaimport com.microsoft.azure.eventhubs.ConnectionStringBuilder; import com.microsoft.azure.eventhubs.EventData; import com.microsoft.azure.eventprocessorhost.CloseReason; import com.microsoft.azure.eventprocessorhost.EventProcessorHost; import com.microsoft.azure.eventprocessorhost.EventProcessorOptions; import com.microsoft.azure.eventprocessorhost.ExceptionReceivedEventArgs; import com.microsoft.azure.eventprocessorhost.IEventProcessor; import com.microsoft.azure.eventprocessorhost.PartitionContext; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; public class EventProcessorSample { public static void main(String args[]) throws InterruptedException, ExecutionException { String consumerGroupName = "$Default"; String namespaceName = "[NAMESPACENAME]"; String eventHubName = "[EVENTHUBNAME]"; String sasKeyName = "[KEYNAME]"; String sasKey = "[KEY]"; String storageConnectionString = "[STORAGECONNECTION]"; String storageContainerName = "[STORAGECONTAINERNAME]"; String hostNamePrefix = ""; ConnectionStringBuilder eventHubConnectionString = new ConnectionStringBuilder() .setNamespaceName(namespaceName) .setEventHubName(eventHubName) .setSasKeyName(sasKeyName) .setSasKey(sasKey); EventProcessorHost host = new EventProcessorHost( EventProcessorHost.createHostName(hostNamePrefix), eventHubName, consumerGroupName, eventHubConnectionString.toString(), storageConnectionString, storageContainerName); System.out.println("Registering host named " + host.getHostName()); EventProcessorOptions options = new EventProcessorOptions(); options.setExceptionNotification(new ErrorNotificationHandler()); host.registerEventProcessor(EventProcessor.class, options) .whenComplete((unused, e) -> { if (e != null) { System.out.println("Failure while registering: " + e.toString()); if (e.getCause() != null) { System.out.println("Inner exception: " + e.getCause().toString()); } } }) .thenAccept((unused) -> { System.out.println("Press enter to stop."); try { System.in.read(); } catch (Exception e) { System.out.println("Keyboard read failed: " + e.toString()); } }) .thenCompose((unused) -> { return host.unregisterEventProcessor(); }) .exceptionally((e) -> { System.out.println("Failure while unregistering: " + e.toString()); if (e.getCause() != null) { System.out.println("Inner exception: " + e.getCause().toString()); } return null; }) .get(); // Wait for everything to finish before exiting main! System.out.println("End of sample"); } // The general notification handler is an object that derives from Consumer<> and takes an ExceptionReceivedEventArgs object // as an argument. The argument provides the details of the error: the exception that occurred and the action (what EventProcessorHost // was doing) during which the error occurred. The complete list of actions can be found in EventProcessorHostActionStrings. public static class ErrorNotificationHandler implements Consumer<ExceptionReceivedEventArgs> { @Override public void accept(ExceptionReceivedEventArgs t) { System.out.println("SAMPLE: Host " + t.getHostname() + " received general error notification during " + t.getAction() + ": " + t.getException().toString()); } } public static class EventProcessor implements IEventProcessor { private int checkpointBatchingCount = 0; // OnOpen is called when a new event processor instance is created by the host. @Override public void onOpen(PartitionContext context) throws Exception { System.out.println("SAMPLE: Partition " + context.getPartitionId() + " is opening"); } // OnClose is called when an event processor instance is being shut down. @Override public void onClose(PartitionContext context, CloseReason reason) throws Exception { System.out.println("SAMPLE: Partition " + context.getPartitionId() + " is closing for reason " + reason.toString()); } // onError is called when an error occurs in EventProcessorHost code that is tied to this partition, such as a receiver failure. @Override public void onError(PartitionContext context, Throwable error) { System.out.println("SAMPLE: Partition " + context.getPartitionId() + " onError: " + error.toString()); } // onEvents is called when events are received on this partition of the Event Hub. @Override public void onEvents(PartitionContext context, Iterable<EventData> events) throws Exception { System.out.println("SAMPLE: Partition " + context.getPartitionId() + " got event batch"); int eventCount = 0; for (EventData data : events) { try { System.out.println("SAMPLE (" + context.getPartitionId() + "," + data.getSystemProperties().getOffset() + "," + data.getSystemProperties().getSequenceNumber() + "): " + new String(data.getBytes(), "UTF8")); eventCount++; // Checkpointing persists the current position in the event stream for this partition and means that the next // time any host opens an event processor on this event hub+consumer group+partition combination, it will start // receiving at the event after this one. this.checkpointBatchingCount++; if ((checkpointBatchingCount % 5) == 0) { System.out.println("SAMPLE: Partition " + context.getPartitionId() + " checkpointing at " + data.getSystemProperties().getOffset() + "," + data.getSystemProperties().getSequenceNumber()); // Checkpoints are created asynchronously. It is important to wait for the result of checkpointing // before exiting onEvents or before creating the next checkpoint, to detect errors and to ensure proper ordering. context.checkpoint(data).get(); } } catch (Exception e) { System.out.println("Processing failed for an event: " + e.toString()); } } System.out.println("SAMPLE: Partition " + context.getPartitionId() + " batch size was " + eventCount + " for host " + context.getOwner()); } } }こちらのコードを実行することでEventhubs に送信されたデータをリアルタイムに受信することができます。
私が詰まったところ
- SASTokenを作成するところでResourceUriが何を指しているかわからなかった。
- SasTokenがどこからどこまでかわからなかった。srからなのか、=の後からなのか全体なのか。 (SharedAccessSignature sr=...)
- メッセージを送信するときにSasトークンと他になんの情報が必要なのか。
これらをそれぞれ通りの組み合わせでトライエラーをする必要があったので時間と労力がかかった。
この記事を読んでくださったみなさんの何らかの助けとなれば嬉しいです。参考文献
Java を使用して Azure Event Hubs との間でイベントを送受信する
Shared Access Signature (SAS) を使用して Event Hubs リソースへのアクセスを認証する最後に
最後まで読んでいただきありがとうございます!
疑問点などございましたら、コメントお待ちしております。
- 投稿日:2020-01-24T03:08:35+09:00
アルゴリズム 体操18
Reverse k Elements
単一リンクリストと整数「k」が引数として渡されます。リストのk要素ずつ反転させるアルゴリズム体操。
k <= 1の場合、リストは変更されません。k >= n(nはリンクリストの長さ)の場合、リンクリスト全体を逆にします。例
以下は、k = 3で、3要素ごとに反転した例です。
以下は、k = 4で、4要素ごとに反転した例です。
Solution
比較的に、単純な問題ですが、コード自体は、いくつかのポインターで追跡する必要があるため、少し複雑です。
このアルゴリズムにおいてのポイントは『2つの反転リストを使う』ことです。
一つ目は最終的に返り値として返す、全体反転リスト。
二つ目が全体反転リストに繋げていく、部分(k要素)反転リスト。
- reversed:全体反転リストの先頭を指すポインタ。返り値になります。
- current_head:サイズ「k」の部分反転リストの先頭。
- current_tail:サイズ 'k'の部分反転リストの末尾。
- prev_tail:既に処理された全体反転リストの末尾。
k要素ずつの部分反転リストを作って、全体反転リストに繋げていく感じになります。
実装
ReverseK.javaclass ReverseK{ LinkedListNode reverse_k_nodes(LinkedListNode head, int k) { if (k <= 1 || head == null) { return head; } LinkedListNode reversed = null; LinkedListNode prev_tail = null; while (head != null & k > 0) { LinkedListNode current_head = null; LinkedListNode current_tail = head; int n = k; while (head != null && n > 0) { LinkedListNode temp = head.next; head.next = current_head; current_head = head; head = temp; n--; } if (reversed == null) { reversed = current_head; } if (prev_tail != null) { prev_tail.next = current_head; } prev_tail = current_tail; } return reversed; } }