Недавно я изучал React и Spring Boot и был бы очень признателен за ваши отзывы о разработанном мной практическом приложении. Приложение состоит из бэкэнда Spring Boot, использующего базу данных H2, а также внешнего интерфейса ReactJS, реализованного на TypeScript. Эти слои взаимодействуют друг с другом через REST API.
API представляет собой базовую систему учетных записей, включающую такие поля, как имя пользователя, пароль и пол. Он также имеет некоторую внутреннюю проверку для обеспечения целостности данных. Также для каждого пользователя создается уникальный идентификатор.
Процесс создания идентификатора в приложении состоит из двух этапов. Во-первых, библиотека Jdenticon используется для создания SVG-файла, представляющего уникальный идентификатор на основе заданного имени пользователя. Затем этот файл SVG преобразуется в изображение PNG с помощью Apache Batik PNGTranscoder, в результате чего изображение представляется в виде массива байтов. Серверная часть предоставляет этот массив байтов в качестве ответа, когда внешний интерфейс запрашивает идентификатор, а внешний интерфейс использует его как большой двоичный объект для отображения изображения идентификатора с использованием элемента img с URL-адресом большого двоичного объекта.
Обратите внимание, что код довольно длинный из-за наличия двух отдельных кодовых баз.
Вот контроллер REST:
@RestController
@CrossOrigin
@RequestMapping("/accounts")
public class AccountController {
private final AccountService accountService;
@Autowired
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
@GetMapping
public ResponseEntity<List<Account>> getAccounts() {
try {
List<Account> accounts = accountService.getAccounts();
return ResponseEntity.ok(accounts);
} catch (Exception ex) {
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/identicon/{username}")
public ResponseEntity<byte[]> getIdenticon(@PathVariable String username) {
try {
byte[] identiconOutput = accountService.getIdenticon(username);
return ResponseEntity.ok(identiconOutput);
} catch (Exception ex) {
return ResponseEntity.internalServerError().build();
}
}
@PostMapping
public ResponseEntity<?> createAccount(@RequestBody Account account) {
try {
accountService.registerAccount(account);
return ResponseEntity.ok().build();
} catch (AccountValidatorException ex) {
return ResponseEntity.badRequest().build();
} catch (Exception ex) {
return ResponseEntity.internalServerError().build();
}
}
@DeleteMapping("/{username}")
public ResponseEntity<?> deleteAccount(@PathVariable String username) {
try {
accountService.deleteAccount(username);
return ResponseEntity.ok().build();
} catch (Exception ex) {
return ResponseEntity.internalServerError().build();
}
}
}
Вот сервис аккаунта:
@Service
public class AccountService {
private final AccountRepository accountRepository;
private final List<AccountValidatorService> accountValidatorServices;
@Autowired
public AccountService(AccountRepository accountRepository, List<AccountValidatorService> accountValidatorServices) {
this.accountRepository = accountRepository;
this.accountValidatorServices = accountValidatorServices;
}
public List<Account> getAccounts() {
return accountRepository.getAccounts();
}
public void registerAccount(Account account) throws AccountValidatorException {
List<String> messages = new ArrayList<>();
for (AccountValidatorService accountValidatorService : accountValidatorServices) {
if (!accountValidatorService.validate(account)) {
messages.add(accountValidatorService.getErrorMessage());
}
}
if (!messages.isEmpty()) {
throw new AccountValidatorException("Error trying to register account: " + messages);
}
accountRepository.addAccount(account.getUsername(), account.getPassword(), account.getGender());
}
public byte[] getIdenticon(String username) throws TranscoderException, IOException {
// get the original SVG XML file which will be converted to a PNG with Apache Batik
String svg = Jdenticon.toSvg(username);
TranscoderInput input = new TranscoderInput(new StringReader(svg));
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
TranscoderOutput output = new TranscoderOutput(byteStream);
PNGTranscoder transcoder = new PNGTranscoder();
transcoder.transcode(input, output);
byteStream.flush();
byteStream.close();
return byteStream.toByteArray();
}
public void deleteAccount(String username) {
accountRepository.deleteAccountByUsername(username);
}
}
Вот репозиторий аккаунта:
public interface AccountRepository extends CrudRepository<Account, Long> {
@Query("SELECT * FROM account")
List<Account> getAccounts();
@Modifying
@Query("DELETE FROM account WHERE username = :username")
void deleteAccountByUsername(String username);
@Modifying
@Query("INSERT INTO account VALUES(NULL, :username, :password, :gender)")
void addAccount(String username, String password, Gender gender);
}
Вот валидаторы аккаунта:
public interface AccountValidatorService {
String getErrorMessage();
boolean validate(Account account);
}
@Service
public class PasswordValidatorService implements AccountValidatorService {
@Override
public String getErrorMessage() {
return "Password must contain at least 3 digits, 3 letters, and 3 special characters";
}
@Override
public boolean validate(Account account) {
String password = account.getPassword();
long digitCount = password
.chars()
.filter(Character::isDigit)
.count();
long letterCount = password
.chars()
.filter(Character::isLetter)
.count();
long specialCharacterCount = 0;
Set<Character> specialCharacters = Set.of('!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', '-');
for (Character c : password.toCharArray()) {
if (specialCharacters.contains(c)) {
++specialCharacterCount;
}
}
return digitCount >= 3 && letterCount >= 3 && specialCharacterCount >= 3;
}
}
@Service
public class UniqueUsernameValidatorService implements AccountValidatorService {
private final AccountRepository accountRepository;
@Autowired
public UniqueUsernameValidatorService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@Override
public String getErrorMessage() {
return "An account with this username already exists";
}
public boolean validate(String username) {
return accountRepository
.getAccounts()
.stream()
.noneMatch(_account -> _account.getUsername().equals(username));
}
@Override
public boolean validate(Account account) {
return validate(account.getUsername());
}
}
@Service
public class UsernameLengthValidatorService implements AccountValidatorService {
@Override
public String getErrorMessage() {
return "Username must have a length larger than 3 and smaller than or equal to 50";
}
@Override
public boolean validate(Account account) {
String username = account.getUsername();
return username.length() >= 3 && username.length() <= 50;
}
}
Реагировать интерфейс:
import { Button, Flex, Input, Select, Text } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react';
type Gender="MALE" | 'FEMALE'
interface Account {
username: string
password: string
gender: Gender
}
class API {
private constructor() { }
static readonly endpoint="http://localhost:8080/accounts"
static async createAccount(account: Account): Promise<void> {
await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(account)
})
}
static async getIdenticon(username: string): Promise<Blob> {
const result = await fetch(`${this.endpoint}/identicon/${username}`)
const blob = result.blob()
return blob
}
static async getAccounts(): Promise<Account[]> {
const result = await fetch(this.endpoint)
const json: Promise<Account[]> = await result.json()
return json
}
static async deleteAccount(account: Account): Promise<void> {
await fetch(`${this.endpoint}/${account.username}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(account)
})
}
}
type AccountDeleteAction = (account: Account) => void
interface AccountListItemProps {
account: Account
onDeleteClick: AccountDeleteAction
}
function AccountListItem(props: AccountListItemProps): JSX.Element {
const [imageUrl, setImageUrl] = useState<string | null>(null)
async function getIdenticon(): Promise<void> {
const response = await API.getIdenticon(props.account.username)
const blobUrl = URL.createObjectURL(response)
setImageUrl(blobUrl)
}
if (imageUrl === null) {
getIdenticon()
}
return (
<Flex
shadow='md'
borderRadius="md"
width="100%"
alignItems="center"
paddingRight="16px"
>
<Flex flexDirection='row' width="100%" padding='16px' height="100px">
{imageUrl && <img src={imageUrl} />}
<Flex flexDirection='column' justifyContent="center" marginLeft="16px">
<h1><Text as="span" fontWeight="bold">Username:</Text> {props.account.username}</h1>
<h2><Text as="span" fontWeight="bold">Password:</Text> {props.account.password}</h2>
<h3><Text as="span" fontWeight="bold">Gender:</Text> {props.account.gender}</h3>
</Flex>
</Flex>
<Flex marginLeft="auto">
<Button colorScheme="red" onClick={() => props.onDeleteClick(props.account)}>Delete</Button>
</Flex>
</Flex>
)
}
interface AccountListProps {
accounts: Account[]
onDeleteClick: AccountDeleteAction
}
function AccountList(props: AccountListProps): JSX.Element {
return (
<Flex flexDirection='column' width="100%">
{props.accounts.map((account, i) =>
<AccountListItem
key={i}
account={account}
onDeleteClick={(account) => props.onDeleteClick(account)}
/>
)}
</Flex>
)
}
function App(): JSX.Element {
const [accounts, setAccounts] = useState<Account[]>([])
const [username, setUsername] = useState<string | null>(null)
const [password, setPassword] = useState<string | null>(null)
const [gender, setGender] = useState<Gender | null>(null)
const [isLoading, setIsLoading] = useState(true)
async function fetchAccounts(): Promise<void> {
const data = await API.getAccounts()
setAccounts(data)
setIsLoading(false)
}
async function formSubmitHandler(e: React.FormEvent<HTMLFormElement>): Promise<void> {
e.preventDefault()
if (username === null || password === null || gender === null) {
return
}
const account: Account = {
username: username,
password: password,
gender: gender
}
setIsLoading(true)
await API.createAccount(account)
setUsername(null)
setPassword(null)
setGender(null)
await fetchAccounts()
setIsLoading(false)
}
useEffect(() => {
fetchAccounts()
}, [])
return (
<Flex padding='16px' height="100vhw" flexDirection='column'>
<Text fontSize="32px" marginBottom='8px'>Create Account</Text>
<Flex
flexDirection='column'
padding='16px'
shadow='md'
borderRadius="md"
>
<form onSubmit={formSubmitHandler}>
<Flex flexDirection='column' gap='8px'>
<Input placeholder="Username" value={username ?? ''} onChange={(e) => setUsername(e.target.value)} />
<Input placeholder="Password" type="password" value={password ?? ''} onChange={(e) => setPassword(e.target.value)} />
<Select
value={gender === null ? 0 : gender}
placeholder="Select Gender"
onChange={(e) => setGender(e.target.value as Gender)}
>
<option value="MALE">Male</option>
<option value="FEMALE">Female</option>
</Select>
<Button type="submit" alignSelf="flex-start">Add Account</Button>
</Flex>
</form>
</Flex>
{isLoading && <Flex width="100%" flexGrow={1} alignItems="center" justifyContent="center"><h1>Loading...</h1></Flex>}
<AccountList onDeleteClick={async (account) => {
await API.deleteAccount(account)
fetchAccounts()
}} accounts={accounts} />
</Flex>
)
}
export default App;
Вот результат:
Я также понимаю все последствия для безопасности, поэтому хочу повторить, что это всего лишь практика 🙂
Буду признателен за отзывы по поводу:
Общий дизайн и архитектура: Я хотел бы знать ваше мнение об общем дизайне и архитектуре приложения. Существуют ли какие-либо шаблоны проектирования или архитектурные принципы, которые можно было бы улучшить или использовать более эффективно?
Организация и структура кода: Пожалуйста, ознакомьтесь с организацией и структурой кодовой базы. Есть ли области, в которых код можно было бы лучше организовать или разделить на модули? Существуют ли какие-либо соглашения об именах, которые можно было бы улучшить?
Бэкенд реализация: Я хотел бы получить отзыв о реализации бэкэнда с использованием Spring Boot. Есть ли области, где код можно оптимизировать или упростить? Есть ли потенциальные проблемы с производительностью или масштабируемостью?
Реализация внешнего интерфейса: Оставьте отзыв о внешнем интерфейсе React, реализованном на TypeScript. Существуют ли какие-либо передовые методы или оптимизации, которые можно было бы применить для улучшения внешнего кода? Как пользовательский интерфейс и общий пользовательский опыт?
Дизайн REST API: Ознакомьтесь с конечными точками REST API и их функциональностью. Можно ли внести какие-либо улучшения для повышения удобства использования и эффективности API? Есть ли какие-либо проблемы безопасности, которые необходимо решить?
Проверка данных и обработка ошибок: Оцените механизмы проверки данных и обработки ошибок, реализованные в серверной части. Существуют ли какие-либо потенциальные уязвимости или крайние случаи, которые необходимо обрабатывать более эффективно?
С уважением.

