Web アプリケーションに取り組んでいると想像してみましょう。私の記事では、次のようなこのモザイクの個々の部分を取り上げています。
これらのトピックはどのように役立ちますか? また、これらの例は実際のプロジェクトでの作業に非常に近いものであり、これらのトピックをテストすることは非常に役立ちます。今日は、このモザイクの次の部分であるファイルの操作を取り上げます。今日では、ファイルを操作しないサイト (たとえば、あらゆる種類の Web ショップ、ソーシャル ネットワークなど) が見つからないためです。レビューは例としてアップロード/ダウンロード/削除メソッドを使用して実行されます。複雑にならないように、アプリケーション内のフォルダー (リソース内) に保存します。 その考え方は、いわゆるファイル ストレージ内のファイル自体に加えて、ファイルに関する情報 (サイズ、名前など) を含むエンティティ、つまり事前に作成されたテーブルをデータベースに保存するというものです。つまり、ファイルをロードするとき、このエンティティは非常に役立ちますが、削除するときは、いかなる形でも忘れてはなりません。このテーブルを見てみましょう。 そして、大人と同じように ID を AUTO_INCREMENT に設定して、データベース レベルで識別子を自動的に生成しましょう。まず、構造を見てみましょう。 上記のテーブルの下にあるエンティティは次のとおりです。
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {
private Long id;
private String name;
private Long size;
private String key;
private LocalDate uploadDate;
}
アップロード
コントローラーを見てみましょう。@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
public class FileController {
private final FileService fileService;
@PostMapping
public ResponseEntity<FileInfo> upload(@RequestParam MultipartFile attachment) {
try {
return new ResponseEntity<>(fileService.upload(attachment), HttpStatus.CREATED);
} catch (IOException e) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}
興味深い点: 9 - 形式でファイルを受け入れますMultipartFile
。バイト配列として受け取ることもできますが、MultipartFile
転送されたファイルのさまざまなプロパティを抽出できるため、私はこのオプションの方が好きです。10 - 14 - アクションをラップして、try catch
下位レベルで例外が発生した場合は上位レベルに転送し、応答として 400 エラーを送信します。次にサービスレベルです。
public interface FileService {
FileInfo upload(MultipartFile resource) throws IOException;
実装を見てみましょう:
@Service
@RequiredArgsConstructor
public class FileServiceImpl implements FileService {
private final FileDAO fileDAO;
private final FileManager fileManager;
@Transactional(rollbackFor = {IOException.class})
@Override
public FileInfo upload(MultipartFile resource) throws IOException {
String key = generateKey(resource.getName());
FileInfo createdFile = FileInfo.builder()
.name(resource.getOriginalFilename())
.key(key)
.size(resource.getSize())
.build();
createdFile = fileDAO.create(createdFile);
fileManager.upload(resource.getBytes(), key);
return createdFile;
}
8 - IOException が発生した場合、データベース内のすべての保存がロールバックされます。11 - ファイルを保存するときに、そのファイルに固有のキーを生成します (同じ名前の 2 つのファイルが保存された場合でも、混乱は起こりません)。12 — データベースに保存するエンティティを構築します。17 — 情報を含むエンティティをデータベースに書き込みます。18 - ハッシュされた名前でファイルを保存します。20 — 作成されたエンティティを返しますFileInfo
が、データベース内で生成された ID (これについては後述します) と作成日が含まれます。ファイルへのキーを生成する方法:
private String generateKey(String name) {
return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
ここでは、名前 + 作成日をハッシュ化し、一意性を確保します。Dao レイヤーインターフェイス:
public interface FileDAO {
FileInfo create(FileInfo file);
その実装:
@Repository
@RequiredArgsConstructor
public class FileDAOImpl implements FileDAO {
private static final String CREATE_FILE = "INSERT INTO files_info(file_name, file_size, file_key, upload_date) VALUES (?, ?, ?, ?)";
private final JdbcTemplate jdbcTemplate;
@Override
public FileInfo create(final FileInfo file) {
LocalDate uploadDate = LocalDate.now();
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(x -> {
PreparedStatement preparedStatement = x.prepareStatement(CREATE_FILE, Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, file.getName());
preparedStatement.setLong(2, file.getSize());
preparedStatement.setString(3, file.getKey());
preparedStatement.setDate(4, Date.valueOf(uploadDate));
return preparedStatement;
}, keyHolder);
return file.toBuilder()
.id(keyHolder.getKey().longValue())
.uploadDate(uploadDate)
.build();
}
11 - 保存する日付を作成します。12 - 21 - エンティティを保存しますが、より複雑な方法で、PreparedStatement
生成された ID を取り出せるようにオブジェクトを明示的に作成します (別のリクエストではなく、応答メタデータの形式で取り出されます) )。22 - 26 - 私たちは長年苦労してきたエンティティの構築を完了し、それをトップに渡します(実際、彼はそれを完成させませんが、新しいオブジェクトを作成し、転送されたフィールドに値を入力し、残りを元のフィールドからコピーします)。 。ファイルがどのように保存されるかを見てみましょうFileManager
:
public void upload(byte[] resource, String keyName) throws IOException {
Path path = Paths.get(DIRECTORY_PATH, keyName);
Path file = Files.createFile(path);
FileOutputStream stream = null;
try {
stream = new FileOutputStream(file.toString());
stream.write(resource);
} finally {
stream.close();
}
}
1 — ファイルをバイト配列として受け入れ、それを保存するときの名前 (生成されたキー) を別途受け入れます。2 - 3 - パス (パスにはパスとキーを書きます) とそれに沿ったファイルを作成します。try-finally
6 - 7 - ストリームを作成し、そこにバイトを書き込みます (ストリームが確実に閉じるように、これらすべてをラップします)。ただし、メソッドの多くは IOException をスローする可能性があります。この場合、メソッド ヘッダーで指定された転送のおかげで、それをコントローラーに渡し、ステータス 400 を与えます。Postman で全体をテストしてみましょう: ご覧のとおり、すべて問題なく、応答は 201 です。応答 JSON はデータベース内の永続化エンティティの形式で返されます。ストレージを調べると、DB: が表示さ れます。新しい価値観を持っているということ。(=*;*=)
ダウンロード
コントローラ:@GetMapping(path = "/{id}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<Resource> download(@PathVariable("id") Long id) {
try {
FileInfo foundFile = fileService.findById(id);
Resource resource = fileService.download(foundFile.getKey());
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=" + foundFile.getName())
.body(resource);
} catch (IOException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
また、これをラップしtry-catch
、IOException の場合には 404 応答ステータス (見つからない) を送信します。4 - 隣接する FileInfo エンティティをデータベースから抽出します。5 - エンティティからのキーを使用して、ファイルをダウンロードします。 6-8 - ファイル名をヘッダーに追加して、ファイルを送り返します (これもファイルに関する情報とともにエンティティから取得します)。さらに詳しく見てみましょう。サービスインターフェース:
Resource download(String key) throws IOException;
FileInfo findById(Long fileId);
実装:
@Override
public Resource download(String key) throws IOException {
return fileManager.download(key);
}
@Transactional(readOnly = true)
@Override
public FileInfo findById(Long fileId) {
return fileDAO.findById(fileId);
}
ここには特に興味深いものはありません。ID でエンティティを検索し、ファイルをダウンロードする方法 (おそらく 46 を除く) - 読み取り用のトランザクションがあることをマークします。ダオレベル:
FileInfo findById(Long fileId);
実装:
private static final String FIND_FILE_BY_ID = "SELECT id, file_name, file_size, file_key, upload_date FROM files_info WHERE id = ?";
@Override
public FileInfo findById(Long fileId) {
return jdbcTemplate.queryForObject(FIND_FILE_BY_ID, rowMapper(), fileId);
}
private RowMapper<FileInfo> rowMapper() {
return (rs, rowNum) -> FileInfo.builder()
.id(rs.getLong("id"))
.name(rs.getString("file_name"))
.size(rs.getLong("file_size"))
.key(rs.getString("file_key"))
.uploadDate(rs.getObject("upload_date", LocalDate.class))
.build();
}
4 —jdbcTemplate
と を使用して ID で検索しますRowMapper
。8 - 15 -RowMapper
データベースとモデル フィールドのデータを比較するための、特定のケースの実装。FileManager
ファイルがどのようにロードされるかを見て みましょう。
public Resource download(String key) throws IOException {
Path path = Paths.get(DIRECTORY_PATH + key);
Resource resource = new UrlResource(path.toUri());
if (resource.exists() || resource.isReadable()) {
return resource;
} else {
throw new IOException();
}
}
ファイルをオブジェクトとして返しResource
、キーで検索します。3 -Resource
パス + キーに沿って作成します。4 - 8 — 指定されたパスにあるファイルが空でないことを確認し、それを読み取ります。すべてがOKであればそれを返し、そうでなければIOExceptionを先頭にスローします。Postman でメソッドを確認してみましょう: ご覧のとおり、正常に動作しました))
消去
@DeleteMapping(value = "/{id}")
public ResponseEntity<Void> delete(@PathVariable("id") Long id) {
try {
fileService.delete(id);
return new ResponseEntity<>(HttpStatus.OK);
} catch (IOException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
ここでは特別なことは何もありません。 を使用して失敗した場合にも 404 を返しますtry-catch
。サービスインターフェース:
void delete(Long fileId) throws IOException;
実装:
@Transactional(rollbackFor = {IOException.class})
@Override
public void delete(Long fileId) throws IOException {
FileInfo file = fileDAO.findById(fileId);
fileDAO.delete(fileId);
fileManager.delete(file.getKey());
}
1 - IOException が発生した場合、データ変更 (削除) もロールバックします。5 - ファイルに関する情報をデータベースから削除します。6 - ファイル自体を「ストレージ」から削除します。daoインターフェース:
void delete(Long fileId);
実装:
private static final String DELETE_FILE_BY_ID = "DELETE FROM files_info WHERE id = ?";
@Override
public void delete(Long fileId) {
jdbcTemplate.update(DELETE_FILE_BY_ID, fileId);
}
そのようなことはありません。ただ削除してください。ファイル自体を削除する場合:
public void delete(String key) throws IOException {
Path path = Paths.get(DIRECTORY_PATH + key);
Files.delete(path);
}
}
Postman を使用します: ストレージを調べます: 空です :) 今、データベース内です: すべてが良好であることがわかります))
テスト
のテストを書いてみましょうFileManager
。まず、テスト部分の構造を見てみましょう。mockFile.txt は、ファイル ストレージでの操作をテストするファイルです。testFileStorage はストレージの代わりになります。 FileManagerTest
:
public class FileManagerTest {
private static MultipartFile multipartFile;
private static FileManager manager;
private static FileInfo file;
@BeforeClass
public static void prepareTestData() throws IOException {
file = FileInfo.builder()
.id(9L)
.name("mockFile.txt")
.key("mockFile.txt")
.size(38975L)
.uploadDate(LocalDate.now())
.build();
multipartFile = new MockMultipartFile("mockFile", "mockFile.txt", "txt",
new FileInputStream("src/test/resources/mockFile.txt"));
manager = new FileManager();
}
ここではテストデータの割り当てを確認します。ファイル保存テスト:
@Test
public void uploadTest() throws IOException {
ReflectionTestUtils.setField(manager, "DIRECTORY_PATH", "src/test/resources/testFileStorage/");
manager.upload(multipartFile.getBytes(), "mockFile.txt");
Path checkFile = Paths.get("src/test/resources/testFileStorage/mockFile.txt");
assertThat(Files.exists(checkFile)).isTrue();
assertThat(Files.isRegularFile(checkFile)).isTrue();
assertThat(Files.size(checkFile)).isEqualTo(multipartFile.getSize());
Files.delete(checkFile);
}
3 — テスト リフレクションを使用して、サービス内の定数を変更して、ファイルを保存するパスを設定します。5 — テスト対象のメソッドを呼び出します。7 - 10 — 保存が正しく実行されたことを確認します。11 - 保存したファイルを削除します(痕跡を残さないでください)。
@Test
public void downloadTest() throws IOException {
ReflectionTestUtils.setField(manager, "DIRECTORY_PATH", "src/test/resources/");
Resource resource = manager.download(file.getKey());
assertThat(resource.isFile()).isTrue();
assertThat(resource.getFilename()).isEqualTo(file.getName());
assertThat(resource.exists()).isTrue();
}
ファイルのアップロード テスト: 3 - 再度、 のパスを変更しますFileManager
。5 - テスト中のメソッドを使用します。7 - 9 — 実行結果を確認します。ファイル削除テスト:
@Test
public void deleteTest() throws IOException {
Path checkFile = Paths.get("src/test/resources/testFileStorage/mockFile.txt");
Files.createFile(checkFile);
assertThat(Files.exists(checkFile)).isTrue();
assertThat(Files.isRegularFile(checkFile)).isTrue();
ReflectionTestUtils.setField(manager, "DIRECTORY_PATH", "src/test/resources/testFileStorage/");
manager.delete(file.getKey());
assertThat(Files.notExists(checkFile)).isTrue();
}
9 - 3 - 4 - パスを設定し、ファイルを作成します。5 - 6 — その存在を確認します。9 - 検証可能な方法を使用します。77 - オブジェクトがもう存在しないことを確認します。依存関係の観点から見てみましょう:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13-rc-2</version>
<scope>test</scope>
</dependency>
</dependencies>
今日はこれで終わりです))
GO TO FULL VERSION