diff --git a/chapter-3-spring-boot-web/src/main/java/demo/springboot/domain/Book.java b/chapter-3-spring-boot-web/src/main/java/demo/springboot/domain/Book.java
index 29dda95a..3102a87b 100644
--- a/chapter-3-spring-boot-web/src/main/java/demo/springboot/domain/Book.java
+++ b/chapter-3-spring-boot-web/src/main/java/demo/springboot/domain/Book.java
@@ -60,4 +60,23 @@ public String getIntroduction() {
public void setIntroduction(String introduction) {
this.introduction = introduction;
}
+
+ public Book(Long id, String name, String writer, String introduction) {
+ this.id = id;
+ this.name = name;
+ this.writer = writer;
+ this.introduction = introduction;
+ }
+
+ public Book(Long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public Book(String name) {
+ this.name = name;
+ }
+
+ public Book() {
+ }
}
diff --git a/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/BookService.java b/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/BookService.java
index 8fb63d13..cd895d72 100644
--- a/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/BookService.java
+++ b/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/BookService.java
@@ -42,4 +42,18 @@ public interface BookService {
* @param id 编号
*/
Book findById(Long id);
+
+ /**
+ * 查找书是否存在
+ * @param book
+ * @return
+ */
+ boolean exists(Book book);
+
+ /**
+ * 根据书名获取书籍
+ * @param name
+ * @return
+ */
+ Book findByName(String name);
}
diff --git a/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/impl/BookServiceImpl.java b/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/impl/BookServiceImpl.java
index 8d63aafc..18a5cfe4 100644
--- a/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/impl/BookServiceImpl.java
+++ b/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/impl/BookServiceImpl.java
@@ -2,24 +2,40 @@
import demo.springboot.domain.Book;
import demo.springboot.service.BookService;
+import demo.springboot.web.BookController;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import javax.annotation.PostConstruct;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
/**
* Book 业务层实现
- *
+ *
* Created by bysocket on 27/09/2017.
*/
@Service
public class BookServiceImpl implements BookService {
+ private static final AtomicLong counter = new AtomicLong();
+
+
+ /**
+ * 使用集合模拟数据库
+ */
+ private static List books = new ArrayList<>(
+ Arrays.asList(
+ new Book(counter.incrementAndGet(), "book")));
+
+
// 模拟数据库,存储 Book 信息
// 第五章《数据存储》会替换成 MySQL 存储
- private static Map BOOK_DB = new HashMap<>();
+ private static Map BOOK_DB = new HashMap<>();
@Override
public List findAll() {
@@ -29,23 +45,40 @@ public List findAll() {
@Override
public Book insertByBook(Book book) {
book.setId(BOOK_DB.size() + 1L);
- BOOK_DB.put(book.getId(), book);
+ BOOK_DB.put(book.getId().toString(), book);
return book;
}
@Override
public Book update(Book book) {
- BOOK_DB.put(book.getId(), book);
+ BOOK_DB.put(book.getId().toString(), book);
return book;
}
@Override
public Book delete(Long id) {
- return BOOK_DB.remove(id);
+ return BOOK_DB.remove(id.toString());
}
@Override
public Book findById(Long id) {
- return BOOK_DB.get(id);
+ return BOOK_DB.get(id.toString());
+ }
+
+ @Override
+ public boolean exists(Book book) {
+ return findByName(book.getName()) != null;
+ }
+
+ @Override
+ public Book findByName(String name) {
+
+ for (Book book : books) {
+ if (book.getName().equals(name)) {
+ return book;
+ }
+ }
+
+ return null;
}
}
diff --git a/chapter-3-spring-boot-web/src/main/java/demo/springboot/web/BookController.java b/chapter-3-spring-boot-web/src/main/java/demo/springboot/web/BookController.java
index 3f852496..d9069a34 100644
--- a/chapter-3-spring-boot-web/src/main/java/demo/springboot/web/BookController.java
+++ b/chapter-3-spring-boot-web/src/main/java/demo/springboot/web/BookController.java
@@ -2,8 +2,14 @@
import demo.springboot.domain.Book;
import demo.springboot.service.BookService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
+import org.springframework.web.util.UriComponentsBuilder;
import java.util.List;
@@ -16,6 +22,10 @@
@RequestMapping(value = "/book")
public class BookController {
+
+ private final Logger LOG = LoggerFactory.getLogger(BookController.class);
+
+
@Autowired
BookService bookService;
@@ -43,8 +53,20 @@ public Book getBook(@PathVariable Long id) {
* 通过 @RequestBody 绑定实体参数,也通过 @RequestParam 传递参数
*/
@RequestMapping(value = "/create", method = RequestMethod.POST)
- public Book postBook(@RequestBody Book book) {
- return bookService.insertByBook(book);
+ public ResponseEntity postBook(@RequestBody Book book, UriComponentsBuilder ucBuilder) {
+
+ LOG.info("creating new book: {}", book);
+
+ if (book.getName().equals("conflict")){
+ LOG.info("a book with name " + book.getName() + " already exists");
+ return new ResponseEntity<>(HttpStatus.CONFLICT);
+ }
+
+ bookService.insertByBook(book);
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setLocation(ucBuilder.path("/book/{id}").buildAndExpand(book.getId()).toUri());
+ return new ResponseEntity<>(headers, HttpStatus.CREATED);
}
/**
diff --git a/chapter-3-spring-boot-web/src/test/java/demo/springboot/web/BookControllerTest.java b/chapter-3-spring-boot-web/src/test/java/demo/springboot/web/BookControllerTest.java
new file mode 100644
index 00000000..2344ee1d
--- /dev/null
+++ b/chapter-3-spring-boot-web/src/test/java/demo/springboot/web/BookControllerTest.java
@@ -0,0 +1,198 @@
+package demo.springboot.web;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import demo.springboot.WebApplication;
+import demo.springboot.domain.Book;
+import demo.springboot.service.BookService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.hamcrest.Matchers.*;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = WebApplication.class)
+@AutoConfigureMockMvc
+@TestPropertySource(locations = "classpath:application.properties")
+public class BookControllerTest {
+
+ private MockMvc mockMvc;
+
+ @Mock
+ private BookService bookService;
+
+ @InjectMocks
+ private BookController bookController;
+
+ @Before
+ public void init() {
+ MockitoAnnotations.initMocks(this);
+ mockMvc = MockMvcBuilders
+ .standaloneSetup(bookController)
+ //.addFilters(new CORSFilter())
+ .build();
+ }
+
+
+ @Test
+ public void getBookList() throws Exception {
+ mockMvc.perform(get("/book")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON_UTF8))
+ .andExpect(jsonPath("$", hasSize(0)));
+
+ }
+
+
+ @Test
+ public void test_create_book_success() throws Exception {
+
+ Book book = createOneBook();
+
+ when(bookService.insertByBook(book)).thenReturn(book);
+
+ mockMvc.perform(
+ post("/book/create")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(asJsonString(book)))
+
+ .andExpect(status().isCreated())
+ .andExpect(header().string("location", containsString("/book/1")));
+ }
+
+
+ @Test
+ public void test_create_book_fail_404_not_found() throws Exception {
+
+ Book book = new Book(99L, "conflict");
+
+ when(bookService.exists(book)).thenReturn(true);
+
+ mockMvc.perform(
+ post("/book/create")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(asJsonString(book)))
+ .andExpect(status().isConflict());
+ }
+
+ @Test
+ public void test_get_book_success() throws Exception {
+
+ Book book = new Book(1L, "测试获取一本书", "strongant作者", "社区 www.spring4all.com 出版社出版");
+
+ when(bookService.findById(1L)).thenReturn(book);
+
+ mockMvc.perform(get("/book/{id}", 1L))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
+ .andExpect(jsonPath("$.id", is(1)))
+ .andExpect(jsonPath("$.name", is("测试获取一本书")));
+
+ verify(bookService, times(1)).findById(1L);
+ verifyNoMoreInteractions(bookService);
+ }
+
+ @Test
+ public void test_get_by_id_fail_null_not_found() throws Exception {
+ when(bookService.findById(1L)).thenReturn(null);
+
+ //TODO: 查找不到应该抛出 404 状态码, Demo 待优化
+ mockMvc.perform(get("/book/{id}", 1L))
+ .andExpect(status().isOk())
+ .andExpect(content().string(""));
+
+ verify(bookService, times(1)).findById(1L);
+ verifyNoMoreInteractions(bookService);
+ }
+
+ @Test
+ public void test_update_book_success() throws Exception {
+
+ Book book = createOneBook();
+
+ when(bookService.findById(book.getId())).thenReturn(book);
+ doReturn(book).when(bookService).update(book);
+
+ mockMvc.perform(
+ put("/book/update", book)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(asJsonString(book)))
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void test_update_book_fail_not_found() throws Exception {
+ Book book = new Book(999L, "测试书名1");
+
+ when(bookService.findById(book.getId())).thenReturn(null);
+
+ mockMvc.perform(
+ put("/book/update", book)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(asJsonString(book)))
+ .andExpect(status().isOk())
+ .andExpect(content().string(""));
+ }
+
+ // =========================================== Delete Book ============================================
+
+ @Test
+ public void test_delete_book_success() throws Exception {
+
+ Book book = new Book(1L, "这本书会被删除啦");
+
+ when(bookService.findById(book.getId())).thenReturn(book);
+ doReturn(book).when(bookService).delete(book.getId());
+
+ mockMvc.perform(
+ delete("/book/delete/{id}", book.getId())
+ ).andExpect(status().isOk());
+ }
+
+ @Test
+ public void test_delete_book_fail_404_not_found() throws Exception {
+ Book book = new Book(1L, "这本书会被删除啦");
+
+ when(bookService.findById(book.getId())).thenReturn(null);
+
+ mockMvc.perform(
+ delete("/book/delete/{id}", book.getId()))
+ .andExpect(status().isOk());
+ }
+
+
+ public static String asJsonString(final Object obj) {
+ try {
+ final ObjectMapper mapper = new ObjectMapper();
+ return mapper.writeValueAsString(obj);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Book createOneBook() {
+ Book book = new Book();
+ book.setId(1L);
+ book.setName("测试书名1");
+ book.setIntroduction("这是一本 www.spring4all.com 社区出版的很不错的一本书籍");
+ book.setWriter("strongant");
+ return book;
+ }
+}
\ No newline at end of file
diff --git a/chapter-3-spring-boot-web/src/test/resources/application.properties b/chapter-3-spring-boot-web/src/test/resources/application.properties
new file mode 100644
index 00000000..5ff02851
--- /dev/null
+++ b/chapter-3-spring-boot-web/src/test/resources/application.properties
@@ -0,0 +1 @@
+server.port=9090
\ No newline at end of file
diff --git a/chapter-4-spring-boot-validating-form-input/pom.xml b/chapter-4-spring-boot-validating-form-input/pom.xml
index 35ab84c2..ef6e6230 100644
--- a/chapter-4-spring-boot-validating-form-input/pom.xml
+++ b/chapter-4-spring-boot-validating-form-input/pom.xml
@@ -54,7 +54,6 @@
com.h2database
h2
- runtime
@@ -71,7 +70,7 @@
org.springframework.boot
spring-boot-maven-plugin
- 1.5.1.RELEASE
+ 2.1.3.RELEASE
diff --git a/chapter-4-spring-boot-validating-form-input/src/main/java/spring/boot/core/ValidatingFormInputApplication.java b/chapter-4-spring-boot-validating-form-input/src/main/java/spring/boot/core/ValidatingFormInputApplication.java
index 1825f04f..c6cb5ac1 100644
--- a/chapter-4-spring-boot-validating-form-input/src/main/java/spring/boot/core/ValidatingFormInputApplication.java
+++ b/chapter-4-spring-boot-validating-form-input/src/main/java/spring/boot/core/ValidatingFormInputApplication.java
@@ -1,12 +1,40 @@
package spring.boot.core;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import spring.boot.core.domain.User;
+import spring.boot.core.domain.UserRepository;
@SpringBootApplication
-public class ValidatingFormInputApplication {
+public class ValidatingFormInputApplication implements CommandLineRunner {
- public static void main(String[] args) {
- SpringApplication.run(ValidatingFormInputApplication.class, args);
- }
+
+ private Logger LOG = LoggerFactory.getLogger(ValidatingFormInputApplication.class);
+
+ @Autowired
+ private UserRepository userRepository;
+
+ public static void main(String[] args) {
+ SpringApplication.run(ValidatingFormInputApplication.class, args);
+ }
+
+ @Override
+ public void run(String... args) throws Exception {
+ User user1 = new User("Sergey", 24, "1994-01-01");
+ User user2 = new User("Ivan", 26, "1994-01-01");
+ User user3 = new User("Adam", 31, "1994-01-01");
+ LOG.info("Inserting data in DB.");
+ userRepository.save(user1);
+ userRepository.save(user2);
+ userRepository.save(user3);
+ LOG.info("User count in DB: {}", userRepository.count());
+ LOG.info("User with ID 1: {}", userRepository.findById(1L));
+ LOG.info("Deleting user with ID 2L form DB.");
+ userRepository.deleteById(2L);
+ LOG.info("User count in DB: {}", userRepository.count());
+ }
}
diff --git a/chapter-4-spring-boot-validating-form-input/src/main/java/spring/boot/core/domain/User.java b/chapter-4-spring-boot-validating-form-input/src/main/java/spring/boot/core/domain/User.java
index 535d3a58..45d4aaf4 100644
--- a/chapter-4-spring-boot-validating-form-input/src/main/java/spring/boot/core/domain/User.java
+++ b/chapter-4-spring-boot-validating-form-input/src/main/java/spring/boot/core/domain/User.java
@@ -79,6 +79,15 @@ public void setBirthday(String birthday) {
this.birthday = birthday;
}
+
+ public User(String name, Integer age, String birthday) {
+ this.name = name;
+ this.age = age;
+ this.birthday = birthday;
+ }
+
+ public User() {}
+
@Override
public String toString() {
return "User{" +
diff --git a/chapter-4-spring-boot-validating-form-input/src/main/java/spring/boot/core/web/UserController.java b/chapter-4-spring-boot-validating-form-input/src/main/java/spring/boot/core/web/UserController.java
index 3eb0360a..8a51c844 100644
--- a/chapter-4-spring-boot-validating-form-input/src/main/java/spring/boot/core/web/UserController.java
+++ b/chapter-4-spring-boot-validating-form-input/src/main/java/spring/boot/core/web/UserController.java
@@ -103,7 +103,7 @@ public String putUser(ModelMap map,
/**
* 处理 "/users/{id}" 的 GET 请求,用来删除 User 信息
*/
- @RequestMapping(value = "/delete/{id}", method = RequestMethod.GET)
+ @RequestMapping(value = "/delete/{id}", method = RequestMethod.DELETE)
public String deleteUser(@PathVariable Long id) {
userService.delete(id);
diff --git a/chapter-4-spring-boot-validating-form-input/src/main/resources/application.properties b/chapter-4-spring-boot-validating-form-input/src/main/resources/application.properties
index 51350c65..1a47991d 100644
--- a/chapter-4-spring-boot-validating-form-input/src/main/resources/application.properties
+++ b/chapter-4-spring-boot-validating-form-input/src/main/resources/application.properties
@@ -1,2 +1,15 @@
+## 开启 H2 数据库
+spring.h2.console.enabled=true
+
+## 配置 H2 数据库连接信息
+spring.datasource.url=jdbc:h2:mem:testdb
+spring.datasource.driverClassName=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=
+
+
+
## 是否显示 SQL 语句
-spring.jpa.show-sql=true
\ No newline at end of file
+spring.jpa.show-sql=true
+hibernate.dialect=org.hibernate.dialect.H2Dialect
+hibernate.hbm2ddl.auto=create
diff --git a/chapter-4-spring-boot-validating-form-input/src/test/java/spring/boot/core/web/UserControllerTest.java b/chapter-4-spring-boot-validating-form-input/src/test/java/spring/boot/core/web/UserControllerTest.java
new file mode 100644
index 00000000..08e9af7e
--- /dev/null
+++ b/chapter-4-spring-boot-validating-form-input/src/test/java/spring/boot/core/web/UserControllerTest.java
@@ -0,0 +1,159 @@
+package spring.boot.core.web;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import spring.boot.core.ValidatingFormInputApplication;
+import spring.boot.core.domain.User;
+import spring.boot.core.service.UserService;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+
+import static junit.framework.TestCase.assertNotNull;
+import static junit.framework.TestCase.assertTrue;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.beans.HasPropertyWithValue.hasProperty;
+import static org.junit.Assert.assertEquals;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ValidatingFormInputApplication.class)
+@AutoConfigureMockMvc
+@TestPropertySource(locations = "classpath:application.properties")
+public class UserControllerTest {
+
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private UserService userService;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Test
+ public void getUserList() throws Exception {
+ mockMvc.perform(get("/users"))
+ .andExpect(view().name("userList"))
+ .andExpect(status().isOk())
+ .andDo(print());
+ }
+
+ private User createUser() {
+ User user = new User();
+ user.setName("测试用户");
+ user.setAge(100);
+ user.setBirthday("1994-01-01");
+ return userService.insertByUser(user);
+ }
+
+ @Test
+ public void createUserForm() throws Exception {
+
+ mockMvc.perform(get("/users/create"))
+ .andDo(print())
+ .andExpect(view().name("userForm"))
+ .andExpect(request().attribute("action", "create"))
+ .andDo(print())
+ .andReturn();
+ }
+
+ @Test
+ public void postUser() throws Exception {
+ User user = createUser();
+ assertNotNull(user);
+
+ MultiValueMap parameters = new LinkedMultiValueMap();
+ Map maps = objectMapper.convertValue(user, new TypeReference