Təsəvvür edək ki, veb proqramınız üzərində işləyirsiniz. Məqalələrimdə bu mozaikanın ayrı-ayrı hissələrinə baxıram, məsələn:
- MySql-i əvəz etmək üçün MariaDB istifadə edərək verilənlər bazasının inteqrasiya sınağı
- Çoxdilli tətbiqin həyata keçirilməsi
@Builder(toBuilder = true)
@Getter
@ToString
public class FileInfo {
private Long id;
private String name;
private Long size;
private String key;
private LocalDate uploadDate;
}
Yükləmək
Nəzarətçiyə baxaq:@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);
}
}
Maraqlı şeylər: 9 - Faylı formada qəbul edirik MultipartFile
. Onu bayt massivi kimi də qəbul etmək olar, amma bu seçimi daha çox bəyənirəm, çünki biz MultipartFile
köçürülmüş faylın müxtəlif xassələrini çıxara bilərik. 10 - 14 - biz hərəkətlərimizi try catch
elə bağlayırıq ki, daha aşağı səviyyədə bir istisna baş verərsə, onu yuxarıya yönləndirək və cavab olaraq 400 xəta göndərək. Sonrakı xidmət səviyyəsidir:
public interface FileService {
FileInfo upload(MultipartFile resource) throws IOException;
Həyata keçirilməsinə baxaq:
@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 halında, verilənlər bazasındakı bütün saxlamalarımız geri qaytarılacaq. 11 - biz saxlandıqda fayl üçün unikal olacaq bir açar yaradırıq (eyni adda iki fayl saxlanılsa belə, qarışıqlıq olmayacaq). 12 — verilənlər bazasında saxlamaq üçün obyekt qururuq. 17 — məlumatı olan qurumu verilənlər bazasına daxil edirik. 18 - faylı hashing adı ilə yadda saxlayın. 20 — yaradılan obyekti qaytarırıq FileInfo
, lakin verilənlər bazasında yaradılan id (bu, aşağıda müzakirə olunacaq) və yaradılma tarixi ilə. Fayl üçün açar yaratmaq üsulu:
private String generateKey(String name) {
return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Burada unikallığımızı təmin edəcək ad + yaradılma tarixini hash edirik. Dao qat interfeysi:
public interface FileDAO {
FileInfo create(FileInfo file);
Onun həyata keçirilməsi:
@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 - saxlayacağımız bir tarix yaradın. 12 - 21 - biz obyekti saxlayırıq, lakin daha mürəkkəb bir şəkildə, obyektin açıq şəkildə yaradılması ilə PreparedStatement
yaradılan id çıxarıla bilər (onu ayrıca bir sorğu ilə deyil, cavab metadata şəklində çıxarır) ). 22 - 26 - biz səbirli varlığımızın tikintisini başa çatdırırıq və zirvəyə veririk (əslində, o, onu tamamlamır, lakin yeni bir obyekt yaradır, köçürülmüş sahələri doldurur və qalanını orijinaldan kopyalayır) . Fayllarımızın necə saxlanacağına baxaq 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 — biz faylı bayt massivi kimi qəbul edirik və ayrı-ayrılıqda onun saxlanacağı adı (yaradılmış açarımız). 2 - 3 - bir yol yaradın (və yolda biz yolu və açarımızı yazırıq) və onun boyunca bir fayl. try-finally
6 - 7 - bir axın yaradın və baytlarımızı orada yazın (və axının mütləq bağlanacağına əmin olmaq üçün bütün bunları sarın ). Bununla belə, metodların çoxu IOException-ı ata bilər. Bu halda, metodun başlığında göstərilən yönləndirmə sayəsində biz onu nəzarətçiyə ötürəcəyik və 400 statusunu verəcəyik. Gəlin hər şeyi Postman-da yoxlayaq: Gördüyünüz kimi, hər şey yaxşıdır, cavab 201-dir, cavab JSON verilənlər bazasındakı davamlı varlığımız şəklində gəldi və yaddaşımıza baxsaq: DB: görürük ki, yeni bir dəyərimiz var. (=*;*=)
Yüklə
Nəzarətçi:@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);
}
}
Biz də onu əhatə edirik try-catch
və IOException halında 404 cavab statusu göndəririk (tapılmadı). 4 - bitişik FileInfo obyektini verilənlər bazasından çıxarın. 5 - obyektdən açardan istifadə edərək, faylı endirin 6-8 - faylın adını başlığa əlavə edərək faylı geri göndərin (yenidən fayl haqqında məlumat olan qurumdan əldə edilir). Gəlin daha dərindən nəzər salaq. Xidmət interfeysi:
Resource download(String key) throws IOException;
FileInfo findById(Long fileId);
İcra:
@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);
}
Burada xüsusilə maraqlı bir şey yoxdur: id-yə görə bir obyekti axtarmaq və faylı yükləmək üsulu, bəlkə də 46 istisna olmaqla - oxumaq üçün əməliyyatımız olduğunu qeyd edirik. Dao səviyyəsi:
FileInfo findById(Long fileId);
İcra:
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();
}
jdbcTemplate
4 — və istifadə edərək id ilə axtarın RowMapper
. 8 - 15 - RowMapper
verilənlər bazası və model sahələrindən məlumatları müqayisə etmək üçün xüsusi işimiz üçün tətbiq. Gedək FileManager
və faylımızın necə yükləndiyinə baxaq:
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();
}
}
Faylı obyekt kimi qaytarırıq Resource
və açarla axtarış edəcəyik. 3 - Resource
yol + açarı boyunca yaradın. 4 - 8 — verilən yoldakı faylın boş olmadığını yoxlayırıq və onu oxuyuruq. Hər şey qaydasındadırsa, onu qaytarırıq, yoxsa, yuxarıya IOException atırıq. Postman-da metodumuzu yoxlayaq: Gördüyümüz kimi, hər şey qaydasındadır))
Sil
@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);
}
}
Burada xüsusi bir şey yoxdur: istifadə edilməməsi halında 404-ü də qaytarırıq try-catch
. Xidmət interfeysi:
void delete(Long fileId) throws IOException;
İcra:
@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 - həmçinin IOException baş verdikdə məlumat dəyişikliklərinin (silmələrin) geri qaytarılması. 5 - verilənlər bazasından fayl haqqında məlumatı silin. 6 - faylın özünü "yaddaşımızdan" silin. dao interfeysi:
void delete(Long fileId);
İcra:
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);
}
Belə bir şey yoxdur - sadəcə silin. Faylın özünün silinməsi:
public void delete(String key) throws IOException {
Path path = Paths.get(DIRECTORY_PATH + key);
Files.delete(path);
}
}
Postmandan istifadə edirik: Anbara baxırıq: Boş :) İndi verilənlər bazasında: Hər şeyin yaxşı olduğunu görürük))
Test
Bizim üçün bir test yazmağa çalışaqFileManager
. Əvvəlcə test hissəsinin strukturuna nəzər salaq: mockFile.txt fayl saxlama ilə əməliyyatlarımızı sınaqdan keçirəcəyimiz fayldır. testFileStorage yaddaşımızı əvəz edəcək. 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();
}
Burada test məlumatlarının təyinatını görürük. Fayl saxlama testi:
@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 — test əksindən istifadə edərək, faylın saxlanması yolunu təyin etmək üçün xidmətdəki sabitimizi dəyişdiririk. 5 - sınaqdan keçirilən metodu çağırın. 7 - 10 - qənaətin düzgün icrasını yoxlayın. 11 - saxlanan faylı silin (heç bir iz buraxmamalıyıq).
@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();
}
Fayl yükləmə testi: 3 - yenidən bizim üçün yolu dəyişdirin FileManager
. 5 - sınaqdan keçirilən metoddan istifadə edin. 7 - 9 - icra nəticəsini yoxlayın. Fayl silmə testi:
@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 - yolu təyin edin və fayl yaradın. 5 - 6 - varlığını yoxlayırıq. 9 - yoxlanıla bilən bir üsuldan istifadə edirik. 77 - obyektin artıq orada olmadığını yoxlayırıq. Və asılılıqlar baxımından nələrə sahib olduğumuzu görək:
<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>
Bu gün mənim üçün hamısı budur))
GO TO FULL VERSION