Browse Source

add spring-boot-web-thymeleaf

纯洁的微笑 7 years ago
parent
commit
374a1b819a

+ 2 - 1
README.md

@@ -17,7 +17,8 @@ Spring Boot 使用的各种示例,以最简单、最实用为标准
 - [spring-boot-banner](https://github.com/ityouknow/spring-boot-examples/tree/master/spring-boot-banner):Spring Boot 定制 Banner 示例
 - [spring-boot-docker](https://github.com/ityouknow/spring-boot-examples/tree/master/spring-boot-banner):使用 Docker 部署 Spring Boot 示例
 - [dockercompose-springboot-mysql-nginx](https://github.com/ityouknow/spring-boot-examples/tree/master/dockercompose-springboot-mysql-nginx) :Docker Compose + Spring Boot + Nginx + Mysql 示例  
-- [spring-boot-commandLineRunner](https://github.com/ityouknow/spring-boot-examples/tree/master/spring-boot-commandLineRunner) :Spring Boot 使用 commandLineRunner 实现项目启动时资源初始化示例   
+- [spring-boot-commandLineRunner](https://github.com/ityouknow/spring-boot-examples/tree/master/spring-boot-commandLineRunner) :Spring Boot 使用 commandLineRunner 实现项目启动时资源初始化示例  
+- [spring-boot-web-thymeleaf](https://github.com/ityouknow/spring-boot-examples/tree/master/spring-boot-web-thymeleaf) :Spring Boot 使用 thymeleaf 实现增删改查示例    
 
 
 **参考文章**

+ 3 - 1
README_EN.md

@@ -16,7 +16,9 @@ Spring Boot Examples, Use the simplest and most useful scene demo.
 - [spring-boot-banner](https://github.com/ityouknow/spring-boot-examples/tree/master/spring-boot-banner):Spring Boot Customized Banner 
 - [spring-boot-docker](https://github.com/ityouknow/spring-boot-examples/tree/master/spring-boot-banner):Spring Boot with Docker 
 - [dockercompose-springboot-mysql-nginx](https://github.com/ityouknow/spring-boot-examples/tree/master/dockercompose-springboot-mysql-nginx) :Docker Compose + Spring Boot + Nginx + Mysql example
-- [spring-boot-commandLineRunner](https://github.com/ityouknow/spring-boot-examples/tree/master/spring-boot-commandLineRunner) :Example of resource initialization at project startup using Spring Boot and commandLineRunner
+- [spring-boot-commandLineRunner](https://github.com/ityouknow/spring-boot-examples/tree/master/spring-boot-commandLineRunner) :Example of resource initialization at project startup using Spring Boot and commandLineRunner  
+- [spring-boot-web-thymeleaf](https://github.com/ityouknow/spring-boot-examples/tree/master/spring-boot-web-thymeleaf) :Spring Boot uses thymeleaf to implement addition, deletion, and modification
+
 
 ---
 

+ 40 - 0
spring-boot-web-thymeleaf/pom.xml

@@ -0,0 +1,40 @@
+<?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>
+	<artifactId>spring-boot-web-thymeleaf</artifactId>
+	<name>Spring Boot Web thymeleaf Sample</name>
+	<description>Spring Boot Web thymeleaf Sample</description>
+
+     <parent>
+		<groupId>org.springframework.boot</groupId>
+		<artifactId>spring-boot-starter-parent</artifactId>
+		<version>2.0.0.RELEASE</version>
+	</parent>
+
+	<dependencies>
+		<!-- Compile -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-thymeleaf</artifactId>
+		</dependency>
+		<!-- Test -->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-test</artifactId>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+			</plugin>
+		</plugins>
+	</build>
+</project>

+ 35 - 0
spring-boot-web-thymeleaf/src/main/java/com/neo/ThymeleafApplication.java

@@ -0,0 +1,35 @@
+
+
+package com.neo;
+
+import com.neo.model.Message;
+import com.neo.repository.InMemoryMessageRepository;
+import com.neo.repository.MessageRepository;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.convert.converter.Converter;
+
+@SpringBootApplication
+public class ThymeleafApplication {
+
+	@Bean
+	public MessageRepository messageRepository() {
+		return new InMemoryMessageRepository();
+	}
+
+	@Bean
+	public Converter<String, Message> messageConverter() {
+		return new Converter<String, Message>() {
+			@Override
+			public Message convert(String id) {
+				return messageRepository().findMessage(Long.valueOf(id));
+			}
+		};
+	}
+
+	public static void main(String[] args) {
+		SpringApplication.run(ThymeleafApplication.class, args);
+	}
+
+}

+ 72 - 0
spring-boot-web-thymeleaf/src/main/java/com/neo/controller/MessageController.java

@@ -0,0 +1,72 @@
+
+package com.neo.controller;
+
+import javax.validation.Valid;
+
+import com.neo.model.Message;
+import com.neo.repository.MessageRepository;
+import org.springframework.stereotype.Controller;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+@Controller
+@RequestMapping("/")
+public class MessageController {
+
+	private final MessageRepository messageRepository;
+
+	public MessageController(MessageRepository messageRepository) {
+		this.messageRepository = messageRepository;
+	}
+
+	@GetMapping
+	public ModelAndView list() {
+		Iterable<Message> messages = this.messageRepository.findAll();
+		return new ModelAndView("messages/list", "messages", messages);
+	}
+
+	@GetMapping("{id}")
+	public ModelAndView view(@PathVariable("id") Message message) {
+		return new ModelAndView("messages/view", "message", message);
+	}
+
+	@GetMapping(params = "form")
+	public String createForm(@ModelAttribute Message message) {
+		return "messages/form";
+	}
+
+	@PostMapping
+	public ModelAndView create(@Valid Message message, BindingResult result,
+			RedirectAttributes redirect) {
+		if (result.hasErrors()) {
+			return new ModelAndView("messages/form", "formErrors", result.getAllErrors());
+		}
+		message = this.messageRepository.save(message);
+		redirect.addFlashAttribute("globalMessage", "Successfully created a new message");
+		return new ModelAndView("redirect:/{message.id}", "message.id", message.getId());
+	}
+
+	@RequestMapping("foo")
+	public String foo() {
+		throw new RuntimeException("Expected exception in controller");
+	}
+
+	@GetMapping(value = "delete/{id}")
+	public ModelAndView delete(@PathVariable("id") Long id) {
+		this.messageRepository.deleteMessage(id);
+		Iterable<Message> messages = this.messageRepository.findAll();
+		return new ModelAndView("messages/list", "messages", messages);
+	}
+
+	@GetMapping(value = "modify/{id}")
+	public ModelAndView modifyForm(@PathVariable("id") Message message) {
+		return new ModelAndView("messages/form", "message", message);
+	}
+
+}

+ 52 - 0
spring-boot-web-thymeleaf/src/main/java/com/neo/model/Message.java

@@ -0,0 +1,52 @@
+package com.neo.model;
+
+import java.util.Calendar;
+
+import javax.validation.constraints.NotEmpty;
+
+
+public class Message {
+
+	private Long id;
+
+	@NotEmpty(message = "Text is required.")
+	private String text;
+
+	@NotEmpty(message = "Summary is required.")
+	private String summary;
+
+	private Calendar created = Calendar.getInstance();
+
+	public Long getId() {
+		return this.id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public Calendar getCreated() {
+		return this.created;
+	}
+
+	public void setCreated(Calendar created) {
+		this.created = created;
+	}
+
+	public String getText() {
+		return this.text;
+	}
+
+	public void setText(String text) {
+		this.text = text;
+	}
+
+	public String getSummary() {
+		return this.summary;
+	}
+
+	public void setSummary(String summary) {
+		this.summary = summary;
+	}
+
+}

+ 44 - 0
spring-boot-web-thymeleaf/src/main/java/com/neo/repository/InMemoryMessageRepository.java

@@ -0,0 +1,44 @@
+
+
+package com.neo.repository;
+
+import com.neo.model.Message;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+
+public class InMemoryMessageRepository implements MessageRepository {
+
+	private static AtomicLong counter = new AtomicLong();
+
+	private final ConcurrentMap<Long, Message> messages = new ConcurrentHashMap<>();
+
+	@Override
+	public Iterable<Message> findAll() {
+		return this.messages.values();
+	}
+
+	@Override
+	public Message save(Message message) {
+		Long id = message.getId();
+		if (id == null) {
+			id = counter.incrementAndGet();
+			message.setId(id);
+		}
+		this.messages.put(id, message);
+		return message;
+	}
+
+	@Override
+	public Message findMessage(Long id) {
+		return this.messages.get(id);
+	}
+
+	@Override
+	public void deleteMessage(Long id) {
+		this.messages.remove(id);
+	}
+
+}

+ 16 - 0
spring-boot-web-thymeleaf/src/main/java/com/neo/repository/MessageRepository.java

@@ -0,0 +1,16 @@
+
+package com.neo.repository;
+
+import com.neo.model.Message;
+
+public interface MessageRepository {
+
+	Iterable<Message> findAll();
+
+	Message save(Message message);
+
+	Message findMessage(Long id);
+
+	void deleteMessage(Long id);
+
+}

+ 4 - 0
spring-boot-web-thymeleaf/src/main/resources/application.properties

@@ -0,0 +1,4 @@
+# Allow Thymeleaf templates to be reloaded at dev time
+spring.thymeleaf.cache: false
+server.tomcat.access_log_enabled: true
+server.tomcat.basedir: target/tomcat

+ 8 - 0
spring-boot-web-thymeleaf/src/main/resources/logback.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+	<include resource="org/springframework/boot/logging/logback/base.xml"/>
+
+	<!-- logger name="org.springframework" level="DEBUG"/-->
+
+</configuration>

File diff suppressed because it is too large
+ 5 - 0
spring-boot-web-thymeleaf/src/main/resources/static/css/bootstrap.min.css


BIN
spring-boot-web-thymeleaf/src/main/resources/static/favicon.ico


+ 18 - 0
spring-boot-web-thymeleaf/src/main/resources/templates/fragments.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+	<head th:fragment="head (title)">
+		<title th:text="${title}">Fragments</title>
+		<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"
+			href="../../css/bootstrap.min.css" />
+	</head>
+	<body>
+		<div class="container">
+			<nav th:fragment="navbar" class="navbar navbar-dark bg-primary">
+				<a class="navbar-brand" href="http://www.ityouknow.com">Ityouknow</a>
+				<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
+					<li class="nav-item"><a class="nav-link" th:href="@{/}" href="messages.html">Messages</a></li>
+				</ul>
+			</nav>
+		</div>
+	</body>
+</html>

+ 31 - 0
spring-boot-web-thymeleaf/src/main/resources/templates/messages/form.html

@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+	<head th:replace="fragments :: head(title=~{::title/text()})">
+		<title>Messages : Create</title>
+	</head>
+	<body>
+		<div class="container">
+			<div th:replace="fragments :: navbar"></div>
+			<div class="float-right mt-2">
+				<a class="btn btn-primary btn-sm" th:href="@{/}" href="messages.html"> Messages </a>
+			</div>
+			<h4 class="float-left mt-2">Messages : Create</h4>
+			<div class="clearfix"></div>
+			<form id="messageForm" th:action="@{/(form)}" th:object="${message}" action="#" method="post">
+				<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger" role="alert">
+					<p th:each="error : ${#fields.errors('*')}" class="m-0" th:text="${error}">Validation error</p>
+				</div>
+				<input type="hidden" th:field="*{id}" th:class="${'form-control' + (#fields.hasErrors('id') ? ' is-invalid' : '')}"/>
+				<div class="form-group">
+					<label for="summary">Summary</label>
+					<input type="text" th:field="*{summary}" th:class="${'form-control' + (#fields.hasErrors('summary') ? ' is-invalid' : '')}">
+				</div>
+				<div class="form-group">
+					<label for="text">Message</label>
+					<textarea th:field="*{text}" th:class="${'form-control' + (#fields.hasErrors('text') ? ' is-invalid' : '')}"></textarea>
+				</div>
+				<button type="submit" class="btn btn-primary">Submit</button>
+			</form>
+		</div>
+	</body>
+</html>

+ 36 - 0
spring-boot-web-thymeleaf/src/main/resources/templates/messages/list.html

@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+	<head th:replace="fragments :: head(title=~{::title/text()})">
+		<title>Messages : View all</title>
+	</head>
+	<body>
+		<div class="container">
+			<div th:replace="fragments :: navbar"></div>
+			<div class="float-right mt-2">
+				<a class="btn btn-primary btn-sm" href="form.html" th:href="@{/(form)}">Create Message</a>
+			</div>
+			<h4 class="float-left mt-2">Messages : View all</h4>
+			<table class="table table-bordered table-striped">
+				<thead>
+					<tr>
+						<th>ID</th>
+						<th>Created</th>
+						<th>Summary</th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr th:if="${messages.empty}">
+						<td colspan="3">No messages</td>
+					</tr>
+					<tr th:each="message : ${messages}">
+						<td th:text="${message.id}">1</td>
+						<td th:text="${#calendars.format(message.created)}">July 11,
+							2012 2:17:16 PM CDT</td>
+						<td><a href="view.html" th:href="@{'/' + ${message.id}}"
+							th:text="${message.summary}"> The summary </a></td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+	</body>
+</html>

+ 27 - 0
spring-boot-web-thymeleaf/src/main/resources/templates/messages/view.html

@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+	<head th:replace="fragments :: head(title=~{::title/text()})">
+		<title>Messages : View</title>
+	</head>
+	<body>
+		<div class="container">
+			<div th:replace="fragments :: navbar"></div>
+			<div class="float-right mt-2">
+				<a class="btn btn-primary btn-sm" href="list.html" th:href="@{/}">Messages</a>
+			</div>
+			<h4 class="float-left mt-2">Messages : View</h4>
+			<div class="clearfix"></div>
+			<div class="alert alert-success" th:if="${globalMessage}" th:text="${globalMessage}">Some Success message
+			</div>
+			<div class="card">
+				<div class="card-body">
+					<h4 class="card-title" th:text="${message.id + ': ' + message.summary}">123 - A short summary...</h4>
+					<h6 class="card-subtitle mb-2 text-muted" th:text="${#calendars.format(message.created)}">July 11, 2012 2:17:16 PM CDT</h6>
+					<p class="card-text" th:text="${message.text}">A detailed message that is longer than the summary.</p>
+					<a class="card-link" href="messages" th:href="@{'/delete/' + ${message.id}}">delete</a>
+					<a class="card-link" href="form.html" th:href="@{'/modify/' + ${message.id}}"> modify</a>
+				</div>
+			</div>
+		</div>
+	</body>
+</html>

+ 89 - 0
spring-boot-web-thymeleaf/src/test/java/com/neo/MessageControllerWebTests.java

@@ -0,0 +1,89 @@
+
+package com.neo;
+
+import java.util.regex.Pattern;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.hamcrest.Matchers.containsString;
+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.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@RunWith(SpringRunner.class)
+@WebAppConfiguration
+@ContextConfiguration(classes = ThymeleafApplication.class)
+public class MessageControllerWebTests {
+
+	@Autowired
+	private WebApplicationContext wac;
+
+	private MockMvc mockMvc;
+
+	@Before
+	public void setup() {
+		this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
+	}
+
+	@Test
+	public void testHome() throws Exception {
+		this.mockMvc.perform(get("/")).andExpect(status().isOk())
+				.andExpect(content().string(containsString("<title>Messages")));
+	}
+
+	@Test
+	public void testCreate() throws Exception {
+		this.mockMvc.perform(post("/").param("text", "FOO text").param("summary", "FOO"))
+				.andExpect(status().isFound())
+				.andExpect(header().string("location", RegexMatcher.matches("/[0-9]+")));
+	}
+
+	@Test
+	public void testCreateValidation() throws Exception {
+		this.mockMvc.perform(post("/").param("text", "").param("summary", ""))
+				.andExpect(status().isOk())
+				.andExpect(content().string(containsString("is required")));
+	}
+
+	private static class RegexMatcher extends TypeSafeMatcher<String> {
+		private final String regex;
+
+		public RegexMatcher(String regex) {
+			this.regex = regex;
+		}
+
+		public static org.hamcrest.Matcher<java.lang.String> matches(String regex) {
+			return new RegexMatcher(regex);
+		}
+
+		@Override
+		public boolean matchesSafely(String item) {
+			return Pattern.compile(this.regex).matcher(item).find();
+		}
+
+		@Override
+		public void describeMismatchSafely(String item, Description mismatchDescription) {
+			mismatchDescription.appendText("was \"").appendText(item).appendText("\"");
+		}
+
+		@Override
+		public void describeTo(Description description) {
+			description.appendText("a string that matches regex: ")
+					.appendText(this.regex);
+		}
+	}
+}

+ 57 - 0
spring-boot-web-thymeleaf/src/test/java/com/neo/ThymeleafApplicationTests.java

@@ -0,0 +1,57 @@
+
+package com.neo;
+
+import java.net.URI;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+public class ThymeleafApplicationTests {
+
+	@Autowired
+	private TestRestTemplate restTemplate;
+
+	@LocalServerPort
+	private int port;
+
+	@Test
+	public void testHome() {
+		ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class);
+		assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
+		assertThat(entity.getBody()).contains("<title>Messages");
+		assertThat(entity.getBody()).doesNotContain("layout:fragment");
+	}
+
+	@Test
+	public void testCreate() {
+		MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
+		map.set("text", "FOO text");
+		map.set("summary", "FOO");
+		URI location = this.restTemplate.postForLocation("/", map);
+		assertThat(location.toString()).contains("localhost:" + this.port);
+	}
+
+	@Test
+	public void testCss() {
+		ResponseEntity<String> entity = this.restTemplate.getForEntity(
+				"http://localhost:" + this.port + "/css/bootstrap.min.css", String.class);
+		assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
+		assertThat(entity.getBody()).contains("body");
+	}
+
+}

Some files were not shown because too many files changed in this diff