Bayangkan Anda sedang mengerjakan aplikasi web Anda. Dalam artikel saya, saya melihat masing-masing bagian dari mosaik ini, seperti:
- Pengujian integrasi database menggunakan MariaDB untuk menggantikan MySql
- Implementasi aplikasi multibahasa
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {
private Long id;
private String name;
private Long size;
private String key;
private LocalDate uploadDate;
}
Mengunggah
Mari kita lihat pengontrolnya:@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);
}
}
Beberapa hal menarik: 9 - Kami menerima file dalam bentuk MultipartFile
. Itu juga dapat diterima sebagai array byte, tapi saya lebih menyukai opsi ini, karena kita dapat MultipartFile
mengekstrak berbagai properti dari file yang ditransfer. 10 - 14 - kita menggabungkan tindakan kita try catch
sehingga jika pengecualian terjadi pada tingkat yang lebih rendah, kita meneruskannya lebih tinggi dan mengirimkan kesalahan 400 sebagai respons. Berikutnya adalah tingkat layanan:
public interface FileService {
FileInfo upload(MultipartFile resource) throws IOException;
Mari kita lihat implementasinya:
@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 - jika terjadi IOException, semua simpanan kami di database akan dibatalkan. 11 - kami membuat kunci yang unik untuk file tersebut saat disimpan (bahkan jika dua file dengan nama yang sama disimpan, tidak akan ada kebingungan). 12 - kami membangun entitas untuk disimpan dalam database. 17 - kami memasukkan entitas dengan informasi ke dalam database. 18 - simpan file dengan nama hash. 20 — kami mengembalikan entitas yang dibuat FileInfo
, tetapi dengan id yang dihasilkan dalam database (ini akan dibahas di bawah) dan tanggal pembuatan. Metode untuk menghasilkan kunci ke suatu file:
private String generateKey(String name) {
return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Di sini kita hash nama + tanggal pembuatan, yang akan memastikan keunikan kita. Antarmuka lapisan Dao:
public interface FileDAO {
FileInfo create(FileInfo file);
Implementasinya:
@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 - buat tanggal yang akan kita simpan. 12 - 21 - kami menyimpan entitas, tetapi dengan cara yang lebih kompleks, dengan pembuatan objek secara eksplisit PreparedStatement
sehingga id yang dihasilkan dapat ditarik keluar (menariknya bukan dengan permintaan terpisah, tetapi dalam bentuk metadata respons ). 22 - 26 - kami menyelesaikan pembangunan entitas kami yang telah lama menderita dan memberikannya ke atas (pada kenyataannya, dia tidak menyelesaikannya, tetapi membuat objek baru, mengisi bidang yang ditransfer dan menyalin sisanya dari yang asli) . Mari kita lihat bagaimana file kita akan disimpan di 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 - kami menerima file sebagai array byte dan secara terpisah nama penyimpanannya (kunci yang kami buat). 2 - 3 - buat jalur (dan di jalur tersebut kami menulis jalur ditambah kunci kami) dan file di sepanjang jalur tersebut. 6 - 7 - buat aliran dan tulis byte kami di sana (dan bungkus semua ini try-finally
untuk memastikan aliran pasti akan ditutup). Namun, banyak metode yang dapat menampilkan IOException. Dalam hal ini, berkat penerusan yang ditentukan dalam header metode, kami akan meneruskannya ke pengontrol dan memberikan status 400. Mari kita uji semuanya di Tukang Pos: Seperti yang bisa kita lihat, semuanya baik-baik saja, responsnya adalah 201, respons JSON datang dalam bentuk entitas yang bertahan di database, dan jika kita melihat ke dalam penyimpanan kita: DB: kita akan melihat bahwa kita mempunyai nilai baru. (=*;*=)
Unduh
Pengendali:@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);
}
}
Kami juga membungkusnya try-catch
dan dalam kasus IOException kami mengirimkan status respons 404 (tidak ditemukan). 4 - mengeluarkan entitas FileInfo yang berdekatan dari database. 5 - menggunakan kunci dari entitas, unduh file 6-8 - kirim kembali file, tambahkan nama file ke header (sekali lagi, diperoleh dari entitas dengan informasi tentang file). Mari kita lihat lebih dalam. Antarmuka layanan:
Resource download(String key) throws IOException;
FileInfo findById(Long fileId);
Penerapan:
@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);
}
Tidak ada yang menarik di sini: metode mencari entitas berdasarkan id dan mengunduh file, kecuali mungkin 46 - kami menandai bahwa kami memiliki transaksi untuk dibaca. tingkat Dao:
FileInfo findById(Long fileId);
Penerapan:
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 — mencari berdasarkan id menggunakan jdbcTemplate
dan RowMapper
. 8 - 15 - implementasi RowMapper
untuk kasus khusus kami, untuk membandingkan data dari database dan bidang model. Ayo pergi FileManager
dan lihat bagaimana file kita dimuat:
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();
}
}
Kami mengembalikan file sebagai objek Resource
, dan kami akan mencari berdasarkan kunci. 3 - buat Resource
di sepanjang jalur + kunci. 4 - 8 — kami memeriksa apakah file pada jalur yang diberikan tidak kosong dan membacanya. Jika semuanya baik-baik saja, kami mengembalikannya, dan jika tidak, kami melemparkan IOException ke atas. Mari kita periksa metode kita di Tukang Pos: Seperti yang Anda lihat, ini berfungsi dengan baik))
Menghapus
@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);
}
}
Tidak ada yang istimewa di sini: kami juga mengembalikan 404 jika terjadi kegagalan penggunaan try-catch
. Antarmuka layanan:
void delete(Long fileId) throws IOException;
Penerapan:
@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 - juga mengembalikan perubahan data (penghapusan) ketika IOException terjadi. 5 - hapus informasi tentang file dari database. 6 - hapus file itu sendiri dari "penyimpanan" kami. antarmuka dao:
void delete(Long fileId);
Penerapan:
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);
}
Tidak ada yang seperti itu - hapus saja. Menghapus file itu sendiri:
public void delete(String key) throws IOException {
Path path = Paths.get(DIRECTORY_PATH + key);
Files.delete(path);
}
}
Kami menggunakan Tukang Pos: Kami mencari di penyimpanan: Kosong :) Sekarang di database: Kami melihat semuanya baik-baik saja))
Tes
Mari kita coba menulis tes untuk kitaFileManager
. Pertama, mari kita lihat struktur bagian pengujian: mockFile.txt adalah file yang akan kita gunakan untuk menguji operasi penyimpanan file kita. testFileStorage akan menjadi pengganti penyimpanan kami. 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();
}
Di sini kita melihat penugasan data pengujian. Tes penyimpanan file:
@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 - menggunakan refleksi pengujian, kami mengubah konstanta kami di layanan untuk mengatur jalur penyimpanan file. 5 — memanggil metode yang sedang diuji. 7 - 10 - periksa kebenaran pelaksanaan penyimpanan. 11 - hapus file yang disimpan (kita tidak boleh meninggalkan jejak apa pun).
@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();
}
Tes unggah file: 3 - sekali lagi, ubah jalur untuk file FileManager
. 5 - gunakan metode yang sedang diuji. 7 - 9 — periksa hasil eksekusi. Tes penghapusan file:
@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 - atur jalur dan buat file. 5 - 6 - kami memeriksa keberadaannya. 9 - kami menggunakan metode yang dapat diverifikasi. 77 - kami memeriksa bahwa objek tersebut sudah tidak ada lagi. Dan mari kita lihat apa yang kita miliki dalam hal ketergantungan:
<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>
Itu saja untuk saya hari ini))
GO TO FULL VERSION