現在、InputStream
を読み取るための以下のコードがあります。ファイル全体をStringBuilder
変数に保存し、その後この文字列を処理しています。
_public static String getContentFromInputStream(InputStream inputStream)
// public static String getContentFromInputStream(InputStream inputStream,
// int maxLineSize, int maxFileSize)
{
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String lineSeparator = System.getProperty("line.separator");
String fileLine;
boolean firstLine = true;
try {
// Expect some function which checks for line size limit.
// eg: reading character by character to an char array and checking for
// linesize in a loop until line feed is encountered.
// if max line size limit is passed then throw an exception
// if a line feed is encountered append the char array to a StringBuilder
// after appending check the size of the StringBuilder
// if file size exceeds the max file limit then throw an exception
fileLine = bufferedReader.readLine();
while (fileLine != null) {
if (!firstLine) stringBuilder.append(lineSeparator);
stringBuilder.append(fileLine);
fileLine = bufferedReader.readLine();
firstLine = false;
}
} catch (IOException e) {
//TODO : throw or handle the exception
}
//TODO : close the stream
return stringBuilder.toString();
}
_
コードはセキュリティチームとのレビューに行き、次のコメントが寄せられました。
_BufferedReader.readLine
_はDOS(Denial of Service)攻撃の影響を受けやすい(無限長の行、改行/キャリッジリターンを含まない巨大なファイル)
StringBuilder
変数のリソースの枯渇(使用可能なメモリより大きいデータを含むファイルの場合)
以下は私が考えることができる解決策です:
readLine
メソッドの代替実装(readLine(int limit)
)を作成し、noをチェックします。読み込まれたバイト数が指定された制限を超えた場合、カスタム例外をスローします。
ファイル全体をロードせずに、ファイルを1行ずつ処理します。 (純粋な非Javaソリューション:))
上記のソリューションを実装する既存のライブラリがあるかどうかを提案してください。また、提案されているものよりも堅牢性が高い、または実装がより便利な代替ソリューションを提案します。パフォーマンスも重要な要件ですが、セキュリティが最初になります。
あらゆる種類のDOS攻撃(行、ファイルのサイズなど)を避けたい。しかし、関数の最後では、ファイル全体を単一のString
!!!に変換しようとしています。行を8 KBに制限すると仮定しますが、誰かが2つの8 KB行を含むファイルを送信するとどうなりますか?行読み取り部分は通過しますが、最終的にすべてを単一の文字列に結合すると、文字列は使用可能なすべてのメモリを詰まらせます。
したがって、最終的にはすべてを単一の文字列に変換するので、行サイズを制限することは重要ではなく、安全でもありません。ファイル全体のサイズを制限する必要があります。
第二に、あなたが基本的にやろうとしていることは、チャンクでデータを読み取ろうとしているということです。したがって、BufferedReader
を使用し、1行ずつ読み取っています。しかし、あなたがやろうとしていること、そして最後に本当に欲しいのは、ファイルを少しずつ読み取る何らかの方法です。一度に1行ずつ読み取るのではなく、一度に2 KBずつ読み取ってください。
BufferedReader
-名前で-内部にバッファがあります。そのバッファーを構成できます。バッファーサイズが2 KBのBufferedReader
を作成するとします。
BufferedReader reader = new BufferedReader(..., 2048);
InputStream
に渡すBufferedReader
に100 KBのデータがある場合、BufferedReader
は一度に2 KBを自動的に読み取ります。そのため、ストリームを50回、それぞれ2 KB(50x2KB = 100 KB)読み取ります。同様に、10 KBのバッファーサイズでBufferedReader
を作成すると、入力を10回読み取ります(10x10KB = 100 KB)。
BufferedReader
は、既にチャンクごとにファイルを読み取る作業を行っています。そのため、その上に行ごとに余分なレイヤーを追加する必要はありません。最終結果に焦点を合わせます-最後のファイルが大きすぎる(>使用可能なRAM)場合-最後にどのようにString
に変換しますか?
より良い方法の1つは、単にCharSequence
として渡すことです。それがAndroidが行うことです。 Android API全体で、CharSequence
がどこでも返されることがわかります。 StringBuilder
はCharSequence
のサブクラスでもあるため、Androidは内部でString
、StringBuilder
、または入力のサイズ/性質に基づいて最適化された他の文字列クラスを使用します。したがって、すべてを読んだら、StringBuilder
に変換するのではなく、String
オブジェクト自体を直接返すことができます。これは、大きなデータに対して安全です。 StringBuilder
も内部のバッファと同じ概念を維持し、1つの長い文字列ではなく、大きな文字列に複数のバッファを内部的に割り当てます。
全体的に:
Apache Commons IOを使用して、次のようにBoundedInputStream
からStringBuilder
にデータを読み取り、行ではなく2 KBブロックで分割します。
// import org.Apache.commons.io.output.StringBuilderWriter;
// import org.Apache.commons.io.input.BoundedInputStream;
// import org.Apache.commons.io.IOUtils;
BoundedInputStream boundedInput = new BoundedInputStream(originalInput, <max-file-size>);
BufferedReader reader = new BufferedReader(new InputStreamReader(boundedInput), 2048);
StringBuilder output = new StringBuilder();
StringBuilderWriter writer = new StringBuilderWriter(output);
IOUtils.copy(reader, writer); // copies data from "reader" => "writer"
return output;
Apache Commons IO ライブラリから BoundedInputStream を使用します。あなたの仕事はずっと簡単になります。
次のコードはあなたが望むことをします:
public static String getContentFromInputStream(InputStream inputStream) {
inputStream = new BoundedInputStream(inputStream, <number-of-bytes>);
// Rest code are all same
InputStream
をBoundedInputStream
で単にラップし、最大サイズを指定するだけです。 BoundedInputStream
は、読み取りをその最大サイズまでに制限します。
または、リーダーを作成するときにこれを行うことができます。
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(
new BoundedInputStream(inputStream, <no-of-bytes>)
)
);
基本的にここで行っていることは、行を読み取るときにサイズを制限するのではなく、InputStream
レイヤー自体で読み取りサイズを制限することです。したがって、InputStreamレイヤーでの読み取りを制限するBoundedInputStream
のような再利用可能なコンポーネントになり、必要な場所で使用できます。
編集:脚注を追加
編集2:コメントに基づいて更新された回答を追加
ファイル処理を行うには、基本的に4つの方法があります。
ストリームベースの処理(_Java.io.InputStream
_モデル):オプションで、ストリームの周囲にbufferedReaderを配置し、ストリームから次の利用可能なテキストを反復して読み取ります(利用可能なテキストがない場合は、blockいくつかのテキストが利用可能になるまで)、テキストの各部分を読み取り中に個別に処理します(さまざまなサイズのテキスト部分を分類します)
チャンクベースの非ブロッキング処理(_Java.nio.channels.Channel
_モデル):一連の固定サイズのバッファー(処理される「チャンク」を表す)を作成し、各バッファーに読み込みますブロックせずにターン(nio APIは高速O/Sレベルスレッドを使用してネイティブIOに委任)、メイン処理スレッドは、他のバッファーが非同期にロードされ続けるため、各バッファーがいっぱいになると順番に固定サイズチャンクを処理します。
パーツファイル処理(行ごとの処理を含む)((1)または(2)を活用して各「パーツ」を分離または構築できます):ファイル形式を意味的に意味のあるサブに分割します-パーツ(可能であれば!行に分割することも可能!)、ストリームピースまたはチャンクを反復処理し、次のパーツが完全にビルドされるまでメモリ内のコンテンツをビルドし、ビルドされるとすぐに各パーツを処理します。
ファイル処理全体(_Java.nio.file.Files
_モデル):1回の操作でファイル全体をメモリに読み込み、内容全体を処理します
どちらを使用すべきですか?
ファイルの内容と必要な処理の種類によって異なります。
リソース使用効率の観点から(最高から最低):1,2,3,4。
処理速度と効率の観点から(最高から最低):2,1,3,4。
プログラミングの容易さの観点から(最高から最低):4,3,1,2。
ただし、処理の種類によっては、最小のテキスト(1、おそらく2)を超えるテキストが必要な場合があり、一部のファイル形式には内部部品がない(3を除外)場合があります。
あなたは4をやっています。3(またはそれ以下)にシフトすることをお勧めします。可能であれば。
4未満では、DOSを回避する方法は1つしかありません。メモリに読み込む前にサイズを制限します(または、ファイルシステムにコピーします)。読み込まれたら手遅れです。これが不可能な場合は、3、2、または1を試してください。
ファイルサイズの制限
多くの場合、ファイルはHTMLフォームを介してアップロードされます。
サーブレット_@MultipartConfig
_アノテーションとrequest.getPart().getInputStream()
を使用してアップロードする場合、ストリームから読み取るデータ量を制御できます。また、request.getPart().getSize()
は事前にファイルサイズを返します。十分に小さい場合は、request.getPart().write(path)
を実行してファイルをディスクに書き込むことができます。
JSFを使用してアップロードする場合、JSF 2.2(非常に新しい)にはmaxLength
の属性を持つ標準のHTMLコンポーネント_<h:inputFile>
_(_javax.faces.component.html.InputFile
_)があります。 JSF 2.2より前の実装には、同様のカスタムコンポーネントがあります(たとえば、トマホークにはmaxLength
属性を持つ_<t:InputFileUpload>
_、PrimeFacesにはsizeLimit
属性を持つ_<p:FileUpload>
_があります)。
ファイル全体を読み取るための代替
InputStream
、StringBuilder
などを使用するコードは、効率的ファイル全体を読み取る方法ですが、必ずしも最も簡単なとは限りません方法(コードの最小行)。
ジュニア/平均的な開発者は、ファイル全体を処理しているときに、効率的なストリームベースの処理をしているという誤解を受ける可能性があるため、適切なコメントを含めてください。
より少ないコードが必要な場合は、次のいずれかを試すことができます。
_ List<String> stringList = Java.nio.file.Files.readAllLines(path, charset);
or
byte[] byteContents = Java.nio.file.Files.readAllBytes(path);
_
しかし、それらには注意が必要です。さもないと、リソースの使用効率が悪くなる可能性があります。 readAllLines
を使用し、List
要素を単一のString
に連結すると、メモリを2倍消費します(List
要素+連結String
)。同様に、readAllBytes
を使用し、続いてString
(new String(byteContents, charset)
)にエンコードすると、再びメモリを「2倍」使用します。ファイルを十分に小さいサイズに制限しない限り、_List<String>
_または_byte[]
_に対して直接処理するのが最善です。
readLineの代わりに、指定された量の文字を読み取るreadを使用します。
各ループで、読み取られたデータの量を確認します。特定の量、予想される入力の最大値を超えている場合、それを停止してエラーを返し、ログに記録します。
追加の注意事項として、BufferedInputStreamを閉じていないことに気付きました。 BufferedReaderはメモリリークの影響を受けやすいため、finally
ブロックを閉じる必要があります。
_...
} catch (IOException e) {
// throw or handle the exception
} finally{
bufferedReader.close();
}
_
new InputStreamReader(inputStream)
を明示的に閉じる必要はありません。これは、ラッピングクラスbufferedReader
を閉じるために呼び出すと自動的に閉じるためです。
巨大なバイナリファイル(通常は改行文字を含まない)をコピーするときに、同様の問題に直面しました。 readline()を実行すると、バイナリファイル全体が単一の文字列に読み込まれ、ヒープスペースでOutOfMemory
が発生します。
次に、簡単なJDKの代替案を示します。
public static void main(String[] args) throws Exception
{
byte[] array = new byte[1024];
FileInputStream fis = new FileInputStream(new File("<Path-to-input-file>"));
FileOutputStream fos = new FileOutputStream(new File("<Path-to-output-file>"));
int length = 0;
while((length = fis.read(array)) != -1)
{
fos.write(array, 0, length);
}
fis.close();
fos.close();
}
注意事項:
上記の例では、1Kバイトのバッファーを使用してファイルをコピーします。ただし、ネットワーク経由でこのコピーを実行している場合は、バッファサイズを微調整することができます。
FileChannel または Commons IO のようなライブラリを使用する場合は、実装が上記のようなものになることを確認してください
これは問題なく機能しました。
char charArray[] = new char[ MAX_BUFFER_SIZE ];
int i = 0;
int c = 0;
while((c = br.read()) != -1 && i < MAX_BUFFER_SIZE) {
char character = (char) c;
charArray[i++] = character;
}
return Arrays.copyOfRange(charArray,0,i);
Apache httpCoreの下にEntityUtilsクラスがあります。このクラスのgetString()メソッドを使用して、応答コンテンツから文字列を取得します。
Apache Commons IO FileUtils。 FileUtilsクラスでは非常にシンプルで、いわゆるDOS攻撃は最上層から直接来ないので、他の選択肢は考えられません。ファイルを書くのはとても簡単です
String content =FileUtils.readFileToString(new File(filePath));
これについて詳しく調べることができます。