Java工程化案例

wen java案例 4

我将为您提供一个完整的Java工程化案例,展示现代Java项目的标准结构和最佳实践。

Java工程化案例

项目实例:在线图书管理系统

项目结构

online-book-store/
├── pom.xml
├── README.md
├── .gitignore
├── .dockerignore
├── docker-compose.yml
├── Dockerfile
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/bookstore/
│   │   │       ├── BookstoreApplication.java
│   │   │       ├── config/
│   │   │       │   ├── RedisConfig.java
│   │   │       │   ├── SwaggerConfig.java
│   │   │       │   └── WebMvcConfig.java
│   │   │       ├── controller/
│   │   │       │   ├── BookController.java
│   │   │       │   ├── UserController.java
│   │   │       │   └── OrderController.java
│   │   │       ├── service/
│   │   │       │   ├── impl/
│   │   │       │   │   ├── BookServiceImpl.java
│   │   │       │   │   └── OrderServiceImpl.java
│   │   │       │   ├── BookService.java
│   │   │       │   └── OrderService.java
│   │   │       ├── repository/
│   │   │       │   ├── BookRepository.java
│   │   │       │   └── OrderRepository.java
│   │   │       ├── entity/
│   │   │       │   ├── Book.java
│   │   │       │   ├── User.java
│   │   │       │   └── Order.java
│   │   │       ├── dto/
│   │   │       │   ├── BookDTO.java
│   │   │       │   ├── CreateOrderRequest.java
│   │   │       │   └── ApiResponse.java
│   │   │       ├── exception/
│   │   │       │   ├── GlobalExceptionHandler.java
│   │   │       │   └── ResourceNotFoundException.java
│   │   │       └── util/
│   │   │           └── JwtUtil.java
│   │   └── resources/
│   │       ├── application.yml
│   │       ├── application-dev.yml
│   │       ├── application-prod.yml
│   │       └── db/
│   │           ├── migration/
│   │           └── seed/
│   └── test/
│       └── java/
│           └── com/example/bookstore/
│               ├── controller/
│               └── service/

pom.xml(核心依赖)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>online-book-store</artifactId>
    <version>1.0.0</version>
    <name>Online Book Store</name>
    <description>图书管理系统</description>
    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>2021.0.3</spring-cloud.version>
        <mapstruct.version>1.5.2.Final</mapstruct.version>
        <lombok.version>1.18.24</lombok.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>
        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</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>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </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>
            <optional>true</optional>
        </dependency>
        <!-- Swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

配置类

application.yml

server:
  port: 8080
  servlet:
    context-path: /api/v1
spring:
  profiles:
    active: dev
  application:
    name: book-store-service
  datasource:
    url: jdbc:mysql://localhost:3306/bookstore
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
        format_sql: true
  redis:
    host: localhost
    port: 6379
    timeout: 5000
jwt:
  secret: ${JWT_SECRET:your-secret-key-here}
  expiration: 86400000
swagger:
  enable: true Book Store API
  version: 1.0.0

实体类与Repository

Book.java

package com.example.bookstore.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "books", indexes = {
    @Index(name = "idx_book_title", columnList = "title"),
    @Index(name = "idx_book_isbn", columnList = "isbn", unique = true)
})
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false, length = 200)
    private String title;
    @Column(nullable = false, length = 13, unique = true)
    private String isbn;
    @Column(nullable = false, length = 100)
    private String author;
    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;
    @Column(nullable = false)
    private Integer stock;
    @Column(columnDefinition = "TEXT")
    private String description;
    @Builder.Default
    @Column(nullable = false)
    private Boolean active = true;
    @CreationTimestamp
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    @UpdateTimestamp
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    @OneToMany(mappedBy = "book", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @Builder.Default
    private List<OrderItem> orderItems = new ArrayList<>();
}

BookRepository.java

package com.example.bookstore.repository;
import com.example.bookstore.entity.Book;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
    Optional<Book> findByIsbn(String isbn);
    Page<Book> findByTitleContaining(String title, Pageable pageable);
    List<Book> findByAuthorContaining(String author);
    @Query(value = "SELECT b FROM Book b WHERE b.stock > 0 AND b.active = true")
    Page<Book> findAvailableBooks(Pageable pageable);
    @Modifying
    @Query("UPDATE Book b SET b.stock = b.stock + :quantity WHERE b.id = :id")
    void updateStock(@Param("id") Long id, @Param("quantity") Integer quantity);
}

Service层

BookService.java

package com.example.bookstore.service;
import com.example.bookstore.dto.BookDTO;
import com.example.bookstore.dto.CreateBookRequest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.List;
public interface BookService {
    BookDTO createBook(CreateBookRequest request);
    BookDTO updateBook(Long id, CreateBookRequest request);
    void deleteBook(Long id);
    BookDTO getBook(Long id);
    List<BookDTO> getBooksByAuthor(String author);
    Page<BookDTO> searchBooks(String keyword, Pageable pageable);
    void updateStock(Long id, int quantity);
    void decrementStock(Long id, int quantity);
}

BookServiceImpl.java

package com.example.bookstore.service.impl;
import com.example.bookstore.dto.BookDTO;
import com.example.bookstore.dto.CreateBookRequest;
import com.example.bookstore.entity.Book;
import com.example.bookstore.exception.ResourceNotFoundException;
import com.example.bookstore.repository.BookRepository;
import com.example.bookstore.service.BookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
@Transactional
public class BookServiceImpl implements BookService {
    private final BookRepository bookRepository;
    public BookServiceImpl(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
    @Override
    @CacheEvict(value = "bookCache", allEntries = true)
    public BookDTO createBook(CreateBookRequest request) {
        log.info("Creating new book: {}", request.getTitle());
        Book book = Book.builder()
            .title(request.getTitle())
            .isbn(request.getIsbn())
            .author(request.getAuthor())
            .price(request.getPrice())
            .stock(request.getStock())
            .description(request.getDescription())
            .build();
        Book savedBook = bookRepository.save(book);
        log.info("Book created successfully with id: {}", savedBook.getId());
        return buildBookDTO(savedBook);
    }
    @Override
    @CacheEvict(value = "bookCache", allEntries = true)
    public BookDTO updateBook(Long id, CreateBookRequest request) {
        Book book = getBookEntity(id);
        book.setTitle(request.getTitle());
        book.setIsbn(request.getIsbn());
        book.setAuthor(request.getAuthor());
        book.setPrice(request.getPrice());
        book.setStock(request.getStock());
        book.setDescription(request.getDescription());
        Book updated = bookRepository.save(book);
        log.info("Book updated successfully with id: {}", id);
        return buildBookDTO(updated);
    }
    @Override
    @CacheEvict(value = "bookCache", allEntries = true)
    public void deleteBook(Long id) {
        Book book = getBookEntity(id);
        book.setActive(false);
        bookRepository.save(book);
        log.info("Book deleted (soft) with id: {}", id);
    }
    @Override
    @Cacheable(value = "bookCache", key = "#id")
    public BookDTO getBook(Long id) {
        log.debug("Fetching book with id: {}", id);
        return buildBookDTO(getBookEntity(id));
    }
    @Override
    public List<BookDTO> getBooksByAuthor(String author) {
        return bookRepository.findByAuthorContaining(author)
            .stream()
            .map(this::buildBookDTO)
            .collect(Collectors.toList());
    }
    @Override
    public Page<BookDTO> searchBooks(String keyword, Pageable pageable) {
        Page<Book> books = bookRepository.findByTitleContaining(keyword, pageable);
        return books.map(this::buildBookDTO);
    }
    @Override
    public void updateStock(Long id, int quantity) {
        bookRepository.updateStock(id, quantity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void decrementStock(Long id, int quantity) {
        Book book = getBookEntity(id);
        if (book.getStock() < quantity) {
            throw new IllegalStateException("Insufficient stock for book: " + id);
        }
        book.setStock(book.getStock() - quantity);
        bookRepository.save(book);
    }
    private Book getBookEntity(Long id) {
        return bookRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Book not found with id: " + id));
    }
    private BookDTO buildBookDTO(Book book) {
        return BookDTO.builder()
            .id(book.getId())
            .title(book.getTitle())
            .isbn(book.getIsbn())
            .author(book.getAuthor())
            .price(book.getPrice())
            .stock(book.getStock())
            .description(book.getDescription())
            .build();
    }
}

Controller层

BookController.java

package com.example.bookstore.controller;
import com.example.bookstore.dto.ApiResponse;
import com.example.bookstore.dto.BookDTO;
import com.example.bookstore.dto.CreateBookRequest;
import com.example.bookstore.service.BookService;
import io.swagger.annotations.*;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.util.List;
@Api(tags = "Book Management")
@RestController
@RequestMapping("/books")
@Validated
public class BookController {
    private final BookService bookService;
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }
    @ApiOperation("Create a new book")
    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<ApiResponse<BookDTO>> createBook(
            @Valid @RequestBody CreateBookRequest request) {
        BookDTO book = bookService.createBook(request);
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(ApiResponse.success("Book created successfully", book));
    }
    @ApiOperation("Get book by ID")
    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<BookDTO>> getBook(
            @PathVariable @NotNull @Min(1) Long id) {
        BookDTO book = bookService.getBook(id);
        return ResponseEntity.ok(ApiResponse.success("Book retrieved successfully", book));
    }
    @ApiOperation("Search books with pagination")
    @GetMapping("/search")
    public ResponseEntity<ApiResponse<Page<BookDTO>>> searchBooks(
            @RequestParam(required = false) String keyword,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "createdAt") String sortBy,
            @RequestParam(defaultValue = "desc") String sortDir) {
        Sort sort = sortDir.equalsIgnoreCase("asc") 
            ? Sort.by(sortBy).ascending() 
            : Sort.by(sortBy).descending();
        PageRequest pageRequest = PageRequest.of(page, size, sort);
        Page<BookDTO> result = bookService.searchBooks(keyword, pageRequest);
        return ResponseEntity.ok(ApiResponse.success("Books retrieved successfully", result));
    }
    @ApiOperation("Update book")
    @PutMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<ApiResponse<BookDTO>> updateBook(
            @PathVariable Long id,
            @Valid @RequestBody CreateBookRequest request) {
        BookDTO book = bookService.updateBook(id, request);
        return ResponseEntity.ok(ApiResponse.success("Book updated successfully", book));
    }
    @ApiOperation("Delete book (soft delete)")
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<ApiResponse<Void>> deleteBook(
            @PathVariable Long id) {
        bookService.deleteBook(id);
        return ResponseEntity.ok(ApiResponse.success("Book deleted successfully", null));
    }
    @ApiOperation("Set stock for a book")
    @PatchMapping("/{id}/stock")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<ApiResponse<Void>> updateStock(
            @PathVariable Long id,
            @RequestParam int quantity) {
        bookService.updateStock(id, quantity);
        return ResponseEntity.ok(ApiResponse.success("Stock updated successfully", null));
    }
}

DTO层

BookDTO.java

package com.example.bookstore.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.*;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BookDTO implements Serializable {
    private Long id;
    @NotBlank(message = "Title is required")
    @Size(max = 200, message = "Title must be less than 200 characters")
    private String title;
    @NotBlank(message = "ISBN is required")
    @Pattern(regexp = "^[0-9-]{10,17}$", message = "Invalid ISBN format")
    private String isbn;
    @NotBlank(message = "Author is required")
    @Size(max = 100, message = "Author must be less than 100 characters")
    private String author;
    @NotNull(message = "Price is required")
    @DecimalMin(value = "0.01", message = "Price must be greater than 0")
    private BigDecimal price;
    @NotNull(message = "Stock is required")
    @Min(value = 0, message = "Stock cannot be negative")
    private Integer stock;
    @Size(max = 1000, message = "Description must be less than 1000 characters")
    private String description;
}

ApiResponse.java

package com.example.bookstore.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
    private boolean success;
    private String message;
    private T data;
    private LocalDateTime timestamp;
    public static <T> ApiResponse<T> success(String message, T data) {
        return ApiResponse.<T>builder()
            .success(true)
            .message(message)
            .data(data)
            .timestamp(LocalDateTime.now())
            .build();
    }
    public static <T> ApiResponse<T> error(String message) {
        return ApiResponse.<T>builder()
            .success(false)
            .message(message)
            .timestamp(LocalDateTime.now())
            .build();
    }
}

异常处理

GlobalExceptionHandler.java

package com.example.bookstore.exception;
import com.example.bookstore.dto.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse<?>> handleResourceNotFound(ResourceNotFoundException ex) {
        log.error("Resource not found: {}", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(ApiResponse.error(ex.getMessage()));
    }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationErrors(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ResponseEntity.badRequest()
            .body(ApiResponse.error(errors.toString()));
    }
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ApiResponse<?>> handleConstraintViolation(ConstraintViolationException ex) {
        return ResponseEntity.badRequest()
            .body(ApiResponse.error(ex.getMessage()));
    }
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ApiResponse<?>> handleAccessDenied(AccessDeniedException ex) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body(ApiResponse.error("Access denied"));
    }
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<?>> handleGenericException(Exception ex) {
        log.error("Unexpected error occurred", ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ApiResponse.error("An unexpected error occurred"));
    }
}

配置类

RedisConfig.java

package com.example.bookstore.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .build();
    }
}

Docker配置

Dockerfile

# Multi-stage build
FROM maven:3.8.4-openjdk-11 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=build /app/target/online-book-store-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]

docker-compose.yml

version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - DB_HOST=mysql
      - REDIS_HOST=redis
    depends_on:
      - mysql
      - redis
  mysql:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=bookstore
      - MYSQL_USER=bookstore
      - MYSQL_PASSWORD=bookstore
    volumes:
      - mysql_data:/var/lib/mysql
    ports:
      - "3306:3306"
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
volumes:
  mysql_data:
  redis_data:

测试类

BookServiceTest.java

package com.example.bookstore.service;
import com.example.bookstore.dto.BookDTO;
import com.example.bookstore.dto.CreateBookRequest;
import com.example.bookstore.entity.Book;
import com.example.bookstore.exception.ResourceNotFoundException;
import com.example.bookstore.repository.BookRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class BookServiceTest {
    @Mock
    private BookRepository bookRepository;
    @InjectMocks
    private BookServiceImpl bookService;
    private Book testBook;
    @BeforeEach
    void setUp() {
        testBook = Book.builder()
            .id(1L)
            .title("Test Book")
            .isbn("978-3-16-148410-0")
            .author("Test Author")
            .price(new BigDecimal("29.99"))
            .stock(100)
            .build();
    }
    @Test
    void createBook_Success() {
        CreateBookRequest request = CreateBookRequest.builder()
            .title("New Book")
            .isbn("978-0-306-40615-7")
            .author("New Author")
            .price(new BigDecimal("39.99"))
            .stock(50)
            .build();
        when(bookRepository.save(any(Book.class))).thenReturn(testBook);
        BookDTO result = bookService.createBook(request);
        assertNotNull(result);
        assertEquals(testBook.getId(), result.getId());
        assertEquals(testBook.getTitle(), result.getTitle());
        verify(bookRepository, times(1)).save(any(Book.class));
    }
    @Test
    void getBook_NotFound_ThrowsException() {
        when(bookRepository.findById(999L)).thenReturn(Optional.empty());
        assertThrows(ResourceNotFoundException.class, () -> {
            bookService.getBook(999L);
        });
        verify(bookRepository, times(1)).findById(999L);
    }
    @Test
    void getBook_Success() {
        when(bookRepository.findById(1L)).thenReturn(Optional.of(testBook));
        BookDTO result = bookService.getBook(1L);
        assertNotNull(result);
        assertEquals("Test Book", result.getTitle());
        assertEquals("978-3-16-148410-0", result.getIsbn());
    }
}

Maven打包和运行

# 清理并打包
mvn clean package
# 跳过测试打包
mvn clean package -DskipTests
# 运行开发环境
mvn spring-boot:run -Dspring-boot.run.profiles=dev
# 运行生产环境
mvn spring-boot:run -Dspring-boot.run.profiles=prod
# Docker构建和运行
docker-compose build
docker-compose up -d

安全配置

SecurityConfig.java

package com.example.bookstore.config;
import com.example.bookstore.util.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/api/v1/auth/**").permitAll()
            .antMatchers("/v3/api-docs/**", "/swagger-ui/**").permitAll()
            .antMatchers(HttpMethod.GET, "/api/v1/books/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

这个完整的工程化案例展示了现代Java开发的各个方面,可以作为实际项目的参考模板,关键要点包括:

  1. 清晰的分层架构:Controller → Service → Repository
  2. DTO模式:避免直接暴露实体类
  3. 异常处理:全局异常处理机制
  4. 缓存优化:Redis缓存策略
  5. 安全控制:JWT认证和角色权限
  6. 配置管理:多环境配置
  7. 容器化部署:Docker支持
  8. 测试覆盖:单元测试示例

抱歉,评论功能暂时关闭!