Mari bayangkan bahawa anda sedang mengusahakan aplikasi web anda. Dalam artikel saya, saya melihat kepingan individu mozek ini, seperti:
- Ujian integrasi pangkalan data menggunakan MariaDB untuk menggantikan MySql
- Pelaksanaan aplikasi berbilang bahasa
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {
private Long id;
private String name;
private Long size;
private String key;
private LocalDate uploadDate;
}
Muat naik
Mari lihat pengawal:@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);
}
}
Perkara yang menarik: 9 - Kami menerima fail dalam borang MultipartFile
. Ia juga boleh diterima sebagai tatasusunan bait, tetapi saya lebih suka pilihan ini, kerana kita boleh MultipartFile
mengekstrak pelbagai sifat fail yang dipindahkan. 10 - 14 - kami membungkus tindakan kami try catch
supaya jika pengecualian berlaku pada tahap yang lebih rendah, kami memajukannya lebih tinggi dan menghantar ralat 400 sebagai respons. Seterusnya ialah tahap perkhidmatan:
public interface FileService {
FileInfo upload(MultipartFile resource) throws IOException;
Mari kita lihat pelaksanaannya:
@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 - dalam kes IOException, semua simpanan kami dalam pangkalan data akan ditarik balik. 11 - kami menjana kunci yang akan menjadi unik untuk fail apabila ia disimpan (walaupun dua fail dengan nama yang sama disimpan, tidak akan ada kekeliruan). 12 — kami membina entiti untuk disimpan dalam pangkalan data. 17 — kami memacu entiti dengan maklumat ke dalam pangkalan data. 18 - simpan fail dengan nama cincang. 20 — kami mengembalikan entiti yang dibuat FileInfo
, tetapi dengan id yang dihasilkan dalam pangkalan data (ini akan dibincangkan di bawah) dan tarikh penciptaan. Kaedah untuk menjana kunci kepada fail:
private String generateKey(String name) {
return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Di sini kami mencincang nama + tarikh penciptaan, yang akan memastikan keunikan kami. Antara muka lapisan Dao:
public interface FileDAO {
FileInfo create(FileInfo file);
Pelaksanaannya:
@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 tarikh yang akan kami simpan. 12 - 21 - kami menyimpan entiti, tetapi dengan cara yang lebih kompleks, dengan penciptaan objek yang jelas PreparedStatement
supaya id yang dihasilkan boleh ditarik keluar (ia mengeluarkannya bukan dengan permintaan yang berasingan, tetapi dalam bentuk metadata tindak balas ). 22 - 26 - kami menyelesaikan pembinaan entiti kami yang telah lama menderita dan memberikannya kepada bahagian atas (sebenarnya, dia tidak menyelesaikannya, tetapi mencipta objek baru, mengisi medan yang dipindahkan dan menyalin yang lain dari yang asal) . Mari lihat bagaimana fail kami akan disimpan dalam 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 fail sebagai tatasusunan bait dan secara berasingan nama di bawahnya ia akan disimpan (kunci hasil kami). 2 - 3 - buat laluan (dan dalam laluan kita tulis laluan ditambah kunci kita) dan fail di sepanjangnya. 6 - 7 - buat strim dan tulis bait kami di sana (dan bungkus semua bahan ini try-finally
untuk memastikan strim itu pasti akan ditutup). Walau bagaimanapun, banyak kaedah boleh membuang IOException. Dalam kes ini, terima kasih kepada pemajuan yang dinyatakan dalam pengepala kaedah, kami akan menyerahkannya kepada pengawal dan memberikan status 400. Mari kita uji semuanya dalam Posman: Seperti yang anda lihat, semuanya baik-baik saja, responsnya ialah 201, respons JSON datang dalam bentuk entiti berterusan kami dalam pangkalan data, dan jika kami melihat ke dalam storan kami: DB: kami melihat bahawa kita ada nilai baru. (=*;*=)
Muat turun
Pengawal:@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 kes IOException kami menghantar status respons 404 (tidak ditemui). 4 - tarik keluar entiti FileInfo bersebelahan daripada pangkalan data. 5 - menggunakan kunci dari entiti, muat turun fail 6-8 - hantar semula fail, menambah nama fail pada pengepala (sekali lagi, diperoleh daripada entiti dengan maklumat tentang fail). Mari kita lihat lebih mendalam. Antara muka perkhidmatan:
Resource download(String key) throws IOException;
FileInfo findById(Long fileId);
Pelaksanaan:
@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);
}
Tiada apa-apa yang menarik di sini: kaedah mencari entiti dengan id dan memuat turun fail, kecuali mungkin 46 - kami menandakan bahawa kami mempunyai transaksi untuk membaca. Tahap Dao:
FileInfo findById(Long fileId);
Pelaksanaan:
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 — cari mengikut id menggunakan jdbcTemplate
dan RowMapper
. 8 - 15 - pelaksanaan RowMapper
untuk kes khusus kami, untuk membandingkan data daripada pangkalan data dan medan model. Mari pergi FileManager
dan lihat bagaimana fail kami dimuatkan:
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 fail sebagai objek Resource
, dan kami akan mencari dengan kunci. 3 - buat Resource
sepanjang laluan + kunci. 4 - 8 — kami menyemak bahawa fail di laluan yang diberikan tidak kosong dan membacanya. Jika semuanya OK, kami mengembalikannya, dan jika tidak, kami membuang IOException ke atas. Mari semak kaedah kami dalam Posman: Seperti yang anda lihat, ia berfungsi OK))
Padam
@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);
}
}
Tiada yang istimewa di sini: kami juga mengembalikan 404 sekiranya berlaku kegagalan menggunakan try-catch
. Antara muka perkhidmatan:
void delete(Long fileId) throws IOException;
Pelaksanaan:
@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 rollback perubahan data (pemadaman) apabila IOException berlaku. 5 - padam maklumat tentang fail daripada pangkalan data. 6 - padamkan fail itu sendiri daripada "storan" kami. antara muka dao:
void delete(Long fileId);
Pelaksanaan:
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);
}
Tiada apa-apa seperti itu - hanya padam. Memadam fail itu sendiri:
public void delete(String key) throws IOException {
Path path = Paths.get(DIRECTORY_PATH + key);
Files.delete(path);
}
}
Kami menggunakan Posmen: Kami melihat dalam storan: Kosong :) Sekarang dalam pangkalan data: Kami melihat bahawa semuanya baik))
Ujian
Mari cuba menulis ujian untuk kamiFileManager
. Mula-mula, mari kita lihat struktur bahagian ujian: mockFile.txt ialah fail yang akan digunakan untuk menguji operasi kami dengan storan fail. testFileStorage akan menjadi pengganti storan 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 tugasan data ujian. Ujian simpanan fail:
@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 ujian, kami menukar pemalar kami dalam perkhidmatan untuk menetapkan laluan untuk menyimpan fail. 5 — panggil kaedah yang diuji. 7 - 10 — kami menyemak pelaksanaan simpan yang betul. 11 - padam fail yang disimpan (kami tidak sepatutnya meninggalkan sebarang kesan).
@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();
}
Ujian muat naik fail: 3 - sekali lagi, tukar laluan untuk FileManager
. 5 - gunakan kaedah yang diuji. 7 - 9 — semak keputusan pelaksanaan. Ujian pemadaman fail:
@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 - tetapkan laluan dan buat fail. 5 - 6 - kita semak kewujudannya. 9 - kami menggunakan kaedah yang boleh disahkan. 77 - kami menyemak sama ada objek itu sudah tiada. Dan mari lihat apa yang kita ada dari segi kebergantungan:
<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 sahaja untuk saya hari ini))
GO TO FULL VERSION