[Java Backend Zero to Hello] [Phase 8] BÀI 8.1: SETUP PROJECT E-COMMERCE
📚 Series: Java Backend Zero to Hello 📂 Phân đoạn: Phase 8: Project thực tế 📖 Nội dung: BÀI 8.1: SETUP PROJECT E-COMMERCE 💡 Khóa học lập trình Backend Java & Spring Boot chuẩn doanh nghiệp từ con số 0.
BÀI 8.1: SETUP PROJECT E-COMMERCE
Mục tiêu
- Khởi tạo project Spring Boot
- Cấu hình dependencies
- Setup Docker Compose
- Cấu hình database
1. TẠO PROJECT
1.1 Spring Initializr
- Project: Maven
- Language: Java
- Spring Boot: 3.2.x
- Java: 17
- Dependencies:
- Spring Web
- Spring Data JPA
- Spring Security
- PostgreSQL Driver
- Spring Data Redis
- Validation
- Lombok
- Spring Boot Actuator
- springdoc-openapi-starter-webmvc-ui
- Java Mail Sender
- Spring Boot DevTools
1.2 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>ecommerce-backend</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>ecommerce-backend</name>
<properties>
<java.version>17</java.version>
<jjwt.version>0.12.3</jjwt.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
</properties>
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Mail -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
<scope>provided</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. CẤU TRÚC PACKAGE
com.example.ecommerce
├── EcommerceApplication.java
├── config/
│ ├── SecurityConfig.java
│ ├── RedisConfig.java
│ ├── OpenApiConfig.java
│ └── CorsConfig.java
├── common/
│ ├── exception/
│ │ ├── GlobalExceptionHandler.java
│ │ ├── NotFoundException.java
│ │ ├── DuplicateException.java
│ │ └── BusinessException.java
│ ├── response/
│ │ ├── ApiResponse.java
│ │ └── PageResponse.java
│ └── util/
├── security/
│ ├── JwtService.java
│ ├── JwtAuthenticationFilter.java
│ └── CustomUserDetailsService.java
├── user/
│ ├── controller/
│ ├── service/
│ ├── repository/
│ ├── entity/
│ └── dto/
├── product/
├── category/
├── cart/
├── order/
├── payment/
└── notification/
3. APPLICATION.YML
3.1 application.yml
spring:
profiles:
active: dev
application:
name: ecommerce-backend
jpa:
open-in-view: false
properties:
hibernate:
format_sql: true
jdbc:
time_zone: UTC
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
data:
redis:
timeout: 2000
server:
port: 8080
servlet:
context-path: /api
app:
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key-here-must-be-at-least-32-characters}
expiration: 86400000 # 24h
refresh-expiration: 604800000 # 7 days
upload:
dir: ${UPLOAD_DIR:./uploads}
cors:
allowed-origins: ${CORS_ORIGINS:http://localhost:3000}
springdoc:
swagger-ui:
path: /swagger-ui.html
operations-sorter: alpha
api-docs:
path: /v3/api-docs
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when-authorized
probes:
enabled: true
3.2 application-dev.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/ecommerce
username: postgres
password: postgres
jpa:
hibernate:
ddl-auto: update
show-sql: true
data:
redis:
host: localhost
port: 6379
logging:
level:
com.example.ecommerce: DEBUG
org.springframework.security: DEBUG
3.3 application-prod.yml
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
minimum-idle: 5
jpa:
hibernate:
ddl-auto: validate
show-sql: false
data:
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
password: ${REDIS_PASSWORD}
logging:
level:
com.example.ecommerce: INFO
4. DOCKER COMPOSE
4.1 docker-compose.yml
version: '3.8'
services:
app:
build: .
container_name: ecommerce-app
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: docker
DB_URL: jdbc:postgresql://db:5432/ecommerce
DB_USERNAME: postgres
DB_PASSWORD: postgres
REDIS_HOST: redis
REDIS_PORT: 6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- uploads:/app/uploads
networks:
- app-network
db:
image: postgres:16-alpine
container_name: ecommerce-db
environment:
POSTGRES_DB: ecommerce
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
redis:
image: redis:7-alpine
container_name: ecommerce-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
volumes:
postgres-data:
redis-data:
uploads:
networks:
app-network:
driver: bridge
4.2 Dockerfile
# Build stage
FROM eclipse-temurin:17-jdk-jammy AS builder
WORKDIR /app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline -B
COPY src ./src
RUN ./mvnw package -DskipTests
# Runtime stage
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
RUN addgroup --system spring && adduser --system spring --ingroup spring
USER spring:spring
COPY /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
5. MAIN APPLICATION
@SpringBootApplication
@EnableJpaAuditing
@EnableAsync
@EnableCaching
@OpenAPIDefinition(
info = @Info(
title = "E-commerce API",
version = "1.0",
description = "Backend API for e-commerce system"
)
)
public class EcommerceApplication {
public static void main(String[] args) {
SpringApplication.run(EcommerceApplication.class, args);
}
}
6. COMMON CLASSES
6.1 ApiResponse
public record ApiResponse<T>(
boolean success,
String message,
T data,
LocalDateTime timestamp
) {
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "Success", data, LocalDateTime.now());
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(true, message, data, LocalDateTime.now());
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, message, null, LocalDateTime.now());
}
}
6.2 Custom Exceptions
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}
public class DuplicateException extends RuntimeException {
public DuplicateException(String message) {
super(message);
}
}
public class BusinessException extends RuntimeException {
private final String code;
public BusinessException(String code, String message) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
}
6.3 Global Exception Handler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleNotFound(NotFoundException ex) {
log.warn("Not found: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error(ex.getMessage()));
}
@ExceptionHandler(DuplicateException.class)
public ResponseEntity<ApiResponse<Void>> handleDuplicate(DuplicateException ex) {
log.warn("Duplicate: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ApiResponse.error(ex.getMessage()));
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException ex) {
log.warn("Business error: {}", ex.getMessage());
return ResponseEntity.badRequest()
.body(ApiResponse.error(ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidation(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(fe -> errors.put(fe.getField(), fe.getDefaultMessage()));
return ResponseEntity.badRequest()
.body(new ApiResponse<>(false, "Validation failed", errors, LocalDateTime.now()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGeneric(Exception ex) {
log.error("Unexpected error", ex);
return ResponseEntity.internalServerError()
.body(ApiResponse.error("Internal server error"));
}
}
7. CHẠY PROJECT
7.1 Local
# Start database
docker-compose up -d db redis
# Run app
mvn spring-boot:run
7.2 Docker
docker-compose up --build
7.3 Truy cập
- API: http://localhost:8080/api
- Swagger: http://localhost:8080/api/swagger-ui.html
- Health: http://localhost:8080/api/actuator/health
8. BÀI TẬP
Bài 1: Setup
- Tạo project với dependencies đầy đủ
- Cấu hình Docker Compose
- Chạy thành công
Bài 2: Common Classes
- Implement ApiResponse
- Implement các Exception
- Implement GlobalExceptionHandler
Bài 3: Test
- Viết test cho GlobalExceptionHandler
- Test các exception
Bài tiếp theo: 8.2 Authentication Module
🧭 Điều hướng bài học
- ⬅️ Bài trước: BÀI 7.7: TRIỂN KHAI LÊN CLOUD
- ➡️ Bài tiếp theo: BÀI 8.2: AUTHENTICATION MODULE
- 📑 Toàn bộ lộ trình: Java Backend Zero to Hello
⭐ Hãy bookmark (clip) lại series để tiện theo dõi các bài học tiếp theo nhé!
All rights reserved