我将为您提供一个完整的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开发的各个方面,可以作为实际项目的参考模板,关键要点包括:
- 清晰的分层架构:Controller → Service → Repository
- DTO模式:避免直接暴露实体类
- 异常处理:全局异常处理机制
- 缓存优化:Redis缓存策略
- 安全控制:JWT认证和角色权限
- 配置管理:多环境配置
- 容器化部署:Docker支持
- 测试覆盖:单元测试示例