Сіз веб-қосымшада жұмыс істеп жатырсыз деп елестетейік. Мен өз мақалаларымда осы мозаиканың жеке бөліктерін қарастырамын, мысалы:
- MySql ауыстыру үшін MariaDB көмегімен дерекқорды интеграциялық тестілеу
- Көптілді қолданбаны жүзеге асыру
@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
. Оны byteтар массиві ретінде де алуға болады, бірақ маған бұл опция көбірек ұнайды, өйткені біз MultipartFile
тасымалданатын файлдың әртүрлі қасиеттерін шығара аламыз. try catch
10 - 14 - егер ерекшелік төменгі деңгейде орын алса, біз оны жоғары жіберіп, жауап ретінде 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 - біз файл сақталған кезде бірегей болатын кілтті жасаймыз (тіпті бірдей атаулары бар екі файл сақталса да, шатасу болмайды). 12 — дерекқорда сақтау үшін нысанды құрастырамыз. 17 — дерекқорға ақпараты бар нысанды енгіземіз. 18 - файлды хэштелген атаумен сақтау. 20 — біз жасалған нысанды қайтарамыз FileInfo
, бірақ дерекқорда жасалған идентификатормен (бұл төменде талқыланады) және жасалған күнімен. Файлдың кілтін жасау әдісі:
private String generateKey(String name) {
return DigestUtils.md5Hex(name + LocalDateTime.now().toString());
}
Мұнда біз атауды + жасау күнін хэштейміз, бұл біздің бірегейлігімізді қамтамасыз етеді. Дао қабатының интерфейсі:
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
(ол оны бөлек сұраумен емес, жауап метадеректері түрінде шығарады. ). 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 — біз файлды byteтар массиві ретінде және ол сақталатын атауды бөлек қабылдаймыз (біздің жасалған кілт). 2 - 3 - жолды жасаңыз (және жолда біз жолды және кілтімізді жазамыз) және оның бойындағы файлды жасаңыз. 6 - 7 - ағын жасаңыз және сол жерге byteтарымызды жазыңыз (және try-finally
ағынның міндетті түрде жабылатынына сенімді болу үшін осы заттардың барлығын ораңыз). Дегенмен, көптеген әдістер 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();
}
jdbcTemplate
4 — және көмегімен идентификатор бойынша іздеу 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 — берілген жолдағы файлдың бос еместігін тексеріп, оны оқимыз. Егер бәрі жақсы болса, біз оны қайтарамыз, ал егер жоқ болса, IOException жоғарғы жағына лақтырамыз. Пошташыдағы әдісімізді тексерейік: Көріп отырғанымыздай, ол жақсы жұмыс істеді))
Жою
@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);
}
}
Біз Пошташыны қолданамыз: Сақтауға қараймыз: Бос :) Енді базада: Барлығы жақсы екенін көреміз))
Сынақ
Бізге тест жазып көрейік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