Java BackEnd-dagi o'zgarishlar

Java BackEnd-dagi o'zgarishlar

Xurshidbek Kurbanov
https://t.me/xurshidbek_kurbanov

Java man uchun asosiy dasturlash tili bo'lib kelgan. Algoritm masalalarini asosan Javada yozaman. Magistraturada o'qiganimda, diplom himoyamdagi tadqiqotning backend qismini JavaEE da amalga oshirgandim. Koreyada birinchi kompaniyamizda backend asosan .NET va Springda yozilar edi. .NET, Android, iOS jamoa a'zolari bilan birgalikda Spring Study Club tashkil qilib, SpringFramework ni o'rganib chiqqandik. O'zimizning kompaniyamizda ish boshlaganimdan buyon SpringBoot da yozib kelaman.

Junior dasturchilarimiz o'zlari bilganicha kod yozib kelgani sababli, kodlarni kelishilgan standartga keltirish uchun vaqt kerak bo'ldi. Har safar yangi loyiha boshlaganimizda birma-bir o'zgarishlar kiritishni boshladim. Hozir esa istalgan yangi loyiha uchun bu o'zgarishlarni qo'llab kelyapmiz. Backendda qilgan dastlabki 5 ta o'zgarishimni Java BackEnd o'qigan yoki o'qiyotgan talabalar bilan bo'lishmoqchiman.


1- Custom Header Class

Odatda, Client va BackEnd ma'lumotlarni muayyan qolip asosida almashadi. API success holatida boshqa, failure holatida esa boshqa turdagi JSON qaytaradigan bo'lsa, bu Client tomonda noqulaylik tug'diradi. Bu muammoni BackEnd tomonda hal qilish mumkin. Ammo, agar Client va BackEnd ma'lumotlarni bir xil qolip asosida ma'lumot almashsa nima bo'ladi? Bu muammoni Custom Class yozish orqali hal qildik.


@PostMapping
public Header<?> addUser(@RequestBody Header<UserAddDTO> dto) {
    return Header.ok(service.add(dto.getData()));
}


public class Header<T> {

    private LocalDateTime transactionTime;

    private String resultCode;

    private String resultMsg;

    private T data;

    private PaginationData pagination;

    public static <T> Header<T> ok() {
        return Header.<T>builder()
                .transactionTime(LocalDateTime.now())
                .resultCode(SUCCESS)
                .resultMsg("OK")
                .build();
    }

    public static <T> Header<T> ok(T data) {
        return Header.<T>builder()
                .transactionTime(LocalDateTime.now())
                .resultCode(SUCCESS)
                .resultMsg("OK")
                .data(data)
                .build();
    }

    public static <T> Header<T> ok(T data, PaginationData pagination) {
        return Header.<T>builder()
                .transactionTime(LocalDateTime.now())
                .resultCode(SUCCESS)
                .resultMsg("OK")
                .data(data)
                .pagination(pagination)
                .build();
    }

    public static <T> Header<T> error() {
        return Header.<T>builder()
                .transactionTime(LocalDateTime.now())
                .resultCode(ERROR)
                .resultMsg("ERROR")
                .build();
    }

    public static <T> Header<T> error(String msg) {
        return Header.<T>builder()
                .transactionTime(LocalDateTime.now())
                .resultCode(ERROR)
                .resultMsg(msg)
                .build();
    }

    public static <T> Header<T> error(String customCode, String msg) {
        return Header.<T>builder()
                .transactionTime(LocalDateTime.now())
                .resultCode(customCode)
                .resultMsg(msg)
                .build();
    }
}


2- Custom HTTP Client Class

Backenddan turib external API larga bog'lanib ma'lumot olish - bu backendda amalga oshiriladigan muhim ishlardan biridir. Agar bir nechta external API bo'lib, har biri uchun alohida kutubxonalar (masalan, RestTemplate, WebClient, FeignClient) ishlatilsa, bu yaxshi emas. Bu yondashuv bilan nechta external API mavjud degan savolga ham javob berish qiyinlashadi. Agar bir kutubxonani olib tashlab boshqasini qo'yishga to'g'ri kelsa ham bir klassning ichki qismini o'gartirish evaziga bunga oson erishiladi. Client da odatda http kutubxonalarini yozilganidek ishlatish emas balki istalgan holatga mos ravishda o'zgartirib olish avzal bo'lib kelgan. Single Responsibility tamoyiliga asoslanib, bu muammoni Custom Class yozish orqali hal qildik.

var response = RestClient.get(RestClient.API_SAMPLE_1,RestClient.paramsEmpty());
System.out.println(response);


public class RestClient {
    private static RestTemplate restTemplate = new RestTemplate();
    private static final ObjectMapper objectMapper = new ObjectMapper();

    private static <T> HttpEntity entity(T input, HttpHeaders headers) {
        return new HttpEntity<T>(input, headers);
    }

    /* Http Methods */
    public static String get(String apiUrl, Map<String, ?> params) {
        try {
            return restTemplate.exchange(apiUrl, HttpMethod.GET, entity("", headers()), String.class, params).getBody();
        } catch (HttpStatusCodeException e) {
            return null;
        }
    }

    public static <T> String post(String apiUrl, T body) {
        try {
            return restTemplate.exchange(apiUrl, HttpMethod.POST, entity(body, headers()), String.class).getBody();
        } catch (HttpStatusCodeException e) {
            return null;
        }
    }

    public static <T> String post(String apiUrl, T body, HttpHeaders headers) {
        try {
            return restTemplate.exchange(apiUrl, HttpMethod.POST, entity(body, headers), String.class).getBody();
        } catch (HttpStatusCodeException e) {
            return null;
        }
    }

    /* Http Headers */
    private static HttpHeaders headers() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(List.of(MediaType.APPLICATION_JSON));
        return headers;
    }

    /* Http APIs */
    public static String API_SAMPLE_1 = "https://sample/api/one";
    public static String API_SAMPLE_2 = "https://sample/api/two";

    /* Http Parameters */
    public static Map<String, ?> paramsEmpty() {
        return new HashMap<>();
    }

    public static Map<String, String> paramsSample() {
        Map<String, String> map = new HashMap<>();
        map.put("serviceKey", "KEY");
        map.put("returnType", "JSON");
        return map;
    }

    /* Http Parsing */
    public static SampleModel parseSample(String jsonStr) {
        try {
            return objectMapper.readValue(jsonStr, SampleModel.class);
        } catch (JsonProcessingException e) {
            return null;
        }
    }
}


3- Request, DTO, Entity, DTO, Response

Client va BackEnd ma'lumotlarni jo'natganda va qabul qilganda e'tibor berilishi kerak bo'lgan bir nechta muhim jihatlar mavjud. Masalan, Entity ni to'g'ridan-to'g'ri Client ga qaytarish eng yomon holatlardan biridir, chunki bu JSON Recursion Problem kabi muammolarni keltirib chiqarishi mumkin. DTO (Data Transfer Object) larni Entity ga aylantirishda (yoki aksincha) MapStruct, Orika kabi mapping frameworklardan foydalanamiz. Ammo, bu frameworklarni chuqur bilmasdan ishlatish yanada ko'proq muammolarni keltirib chiqarishi mumkin. Nima bo'ladi agar Request va Response uchun alohida DTO lar va Entity uchun alohida DTO mavjud bo'lsa? Maqsad esa MapStruct, Orika kabi frameworklardan foydalanmaslik. Bu muammoni record lardan foydalanib quyidagicha hal qildik.


public record ArticleRequest(
        @NotBlank(message = "userId is required")
        @Size(min = 5, max = 20)
        String username,

        @NotBlank(message = "title is required")
        @Size(min = 5, max = 20)
        String title,
        @NotBlank(message = "content is required")
        @Size(min = 5, max = 500)
        String content
) {
    public static ArticleRequest of(String username, String title, String content) {
        return new ArticleRequest(username, title, content);
    }

    public ArticleDto toDto(UserAccountDto userAccountDto) {
        return toDto(userAccountDto, null);
    }

    public ArticleDto toDto(UserAccountDto userAccountDto, Set<HashtagDto> hashtagDtos) {
        return ArticleDto.of(
                userAccountDto,
                title,
                content,
                hashtagDtos
        );
    }
}


public record ArticleResponse(
        Long id,
        String title,
        String content,
        Set<String> hashtags,
        String email,
        String nickname
) {
    public static ArticleResponse of(Long id, String title, String content, Set<String> hashtags, String email, String nickname) {
        return new ArticleResponse(id, title, content, hashtags, email, nickname);
    }

    public static ArticleResponse from(ArticleDto dto) {
        String nickname = dto.userAccountDto().nickname();
        if (nickname == null || nickname.isBlank()) {
            nickname = dto.userAccountDto().username();
        }

        return new ArticleResponse(
                dto.id(),
                dto.title(),
                dto.content(),
                dto.hashtagDtos().stream()
                        .map(HashtagDto::hashtagName)
                        .collect(Collectors.toUnmodifiableSet())
                ,
                dto.userAccountDto().username(),
                nickname
        );
    }
}


public record ArticleDto(
        Long id,
        UserAccountDto userAccountDto,
        String title,
        String content,
        Set<HashtagDto> hashtagDtos
) {
    public static ArticleDto of(UserAccountDto userAccountDto, String title, String content, Set<HashtagDto> hashtagDtos) {
        return new ArticleDto(null, userAccountDto, title, content, hashtagDtos);
    }

    public static ArticleDto of(Long id, UserAccountDto userAccountDto, String title, String content, Set<HashtagDto> hashtagDtos) {
        return new ArticleDto(id, userAccountDto, title, content, hashtagDtos);
    }

    public static ArticleDto from(Article entity) {
        return new ArticleDto(
                entity.getId(),
                UserAccountDto.from(entity.getUserAccount()),
                entity.getTitle(),
                entity.getContent(),
                entity.getHashtags().stream()
                        .map(HashtagDto::from)
                        .collect(Collectors.toUnmodifiableSet())
        );
    }

    public Article toEntity(UserAccount userAccount) {
        return Article.of(
                userAccount,
                title,
                content
        );
    }
}


@Getter
@ToString(callSuper = true)
@Table(indexes = {
        @Index(columnList = "title"),
        @Index(columnList = "createdAt"),
        @Index(columnList = "createdBy")
})
@Entity
public class Article extends AuditingFields {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Setter
    @ManyToOne
    @JoinColumn(name = "userId")
    private UserAccount userAccount;

    @Setter
    @Column(nullable = false)
    private String title;

    @Setter
    @Column(nullable = false, length = 1000)
    private String content;

    @ToString.Exclude
    @JoinTable(
            name = "article_hashtag",
            joinColumns = @JoinColumn(name = "article_id"),
            inverseJoinColumns = @JoinColumn(name = "hashtag_id")
    )
    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private final Set<Hashtag> hashtags = new LinkedHashSet<>();

    @ToString.Exclude
    @OneToMany(mappedBy = "article", cascade = CascadeType.ALL)
    private final Set<Comment> comments = new LinkedHashSet<>();

    protected Article() {
    }

    private Article(UserAccount userAccount, String title, String content) {
        this.userAccount = userAccount;
        this.title = title;
        this.content = content;
    }

    public static Article of(UserAccount userAccount, String title, String content) {
        return new Article(userAccount, title, content);
    }

    public void addHashtag(Hashtag hashtag) {
        this.getHashtags().add(hashtag);
    }

    public void addHashtags(Collection<Hashtag> hashtags) {
        this.getHashtags().addAll(hashtags);
    }

    public void clearHashtags() {
        this.getHashtags().clear();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Article article = (Article) o;
        return Objects.equals(id, article.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}


4- UserDetails vs UserPrinciple

Spring Security da albatta UserDetails klassdan foydalanishga to'g'ri keladi va buni odatda Entity klass bilan birga ishlatamiz. Entity klass bu Database da Table bilan bo'gliq qimmat klass bo'lgani sababli ularni alohida ishlatishimiz maqsadga muvofiq bo'ladi.


public record UserPrinciple(
        String username,
        String password,
        String nickname,
        Set<RoleType> roles
) implements UserDetails {


    public static UserPrinciple of(String username, String password, String nickname, Set<RoleType> roles) {
        return new UserPrinciple(
                username,
                password,
                nickname,
                roles

        );
    }

    public static UserPrinciple from(UserAccount userAccount) {
        return UserPrinciple.of(
                userAccount.getUsername(),
                userAccount.getPassword(),
                userAccount.getNickname(),
                userAccount.getRoles()
        );
    }

    public UserAccount toEntity() {
        UserAccount userAccount = UserAccount.of(username, password, nickname);
        userAccount.setRoles(roles);
        return userAccount;
    }


    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (RoleType role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}


@Getter
@ToString(callSuper = true)
@Table(indexes = {
        @Index(columnList = "username", unique = true),
        @Index(columnList = "createdAt"),
        @Index(columnList = "createdBy")
})
@Entity
public class UserAccount extends AuditingFields {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Setter
    @Column(length = 100)
    private String username;

    @Setter
    @Column(nullable = false)
    private String password;

    @Setter
    @Column(length = 100)
    private String nickname;

    @Setter
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "useraccount_roletype",
            joinColumns = @JoinColumn(name = "useraccount_id"),
            inverseJoinColumns = @JoinColumn(name = "roletype_id")
    )
    private Set<RoleType> roles = new HashSet<>();

    protected UserAccount() {
    }

    private UserAccount(String username, String password, String nickname) {
        this.username = username;
        this.password = password;
        this.nickname = nickname;
    }

    public static UserAccount of(String username, String password, String nickname) {
        return new UserAccount(username, password, nickname);
    }

    public void addRoleType(RoleType roleType){
        this.roles.add(roleType);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserAccount that = (UserAccount) o;
        return Objects.equals(id, that.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}


5- CRUD Controller

Bug'alteriya bilan bog'liq loyihalarda standart CRUD elementlariga ega bo'lgan 20 dan ortiq Entity larni ishlatishga to'g'ri keldi. Har bir holat uchun alohida kod yozish o'rniga, barcha Entity lar uchun umumiy ishlaydigan kod yaratish maqsadga muvofiqdir. Bu yondashuv kodni saqlash, o'zgartirish va kengaytirishni osonlashtiradi.

@RestController
@RequestMapping("/api/partner")
public class PartnerController extends  CrudController<PartnerRequest, PartnerResponse, Partner>{

}


@RestController
@RequestMapping("/api/item")
public class ItemController extends CrudController<ItemRequest, ItemResponse, Item> {

    @Autowired
    ItemService itemService;

    @GetMapping("/search")
    public Header searchItems(@PageableDefault Pageable pageable){
        return itemService.searchItems(pageable);
    }
}


@Component
public abstract class CrudController<Req, Res, Entity> implements CrudInterface<Req, Res> {

    @Autowired(required = false)
    protected BaseService<Req, Res,Entity> baseService;

    @Override
    @PostMapping("")
    public Header<Res> create(@RequestBody Header<Req> request) {
        return baseService.create(request);
    }

    @Override
    @GetMapping("{id}")
    public Header<Res> read(@PathVariable Long id) {
        return baseService.read(id);
    }

    @Override
    @PutMapping("")
    public Header<Res> update(@RequestBody Header<Req> request) {
        return baseService.update(request);
    }

    @Override
    @DeleteMapping("{id}")
    public Header delete(@PathVariable Long id) {
        return baseService.delete(id);
    }
}

🔷 Join - https://t.me/xurshidbek_kurbanov


Report Page