我将为您提供一个完整的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支持
- 测试覆盖:单元测试示例
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播