JAVA-3541: Move spring-5-mvc into spring-web-modules

This commit is contained in:
Krzysztof Woyke
2021-01-06 11:16:55 +01:00
parent 40dbb56721
commit b348dd79c8
28 changed files with 67 additions and 69 deletions

View File

@@ -0,0 +1,20 @@
*.class
# Mobile Tools for Java (J2ME)
.mtj.tmp/
.settings/
.classpath
.project
target/
*.iml
.idea
# Package Files #
*.jar
*.war
*.ear
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

View File

@@ -0,0 +1,8 @@
## Spring 5 MVC
This module contains articles about Spring 5 model-view-controller (MVC) pattern
### Relevant Articles:
- [Spring MVC Streaming and SSE Request Processing](https://www.baeldung.com/spring-mvc-sse-streams)
- [Interface Driven Controllers in Spring](https://www.baeldung.com/spring-interface-driven-controllers)
- [Returning Plain HTML From a Spring MVC Controller](https://www.baeldung.com/spring-mvc-return-html)

View File

@@ -0,0 +1,99 @@
<?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-5-mvc</artifactId>
<name>spring-5-mvc</name>
<packaging>jar</packaging>
<description>spring 5 MVC sample project about new features</description>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../../parent-boot-2</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- utils -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
<!-- runtime and test scoped -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.restassured</groupId>
<artifactId>rest-assured</artifactId>
<version>${jayway-rest-assured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>${javafaker.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
</dependency>
</dependencies>
<build>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<properties>
<jayway-rest-assured.version>2.9.0</jayway-rest-assured.version>
<httpclient.version>4.5.8</httpclient.version>
<start-class>com.baeldung.Spring5Application</start-class>
<javafaker.version>0.18</javafaker.version>
</properties>
</project>

View File

@@ -0,0 +1,21 @@
package com.baeldung;
public class Constants {
public static final String GENERIC_EXCEPTION = "Exception encountered!";
/**
* API endpoints.
*/
public static final String API_RBE = "/rbe";
public static final String API_SSE = "/sse";
public static final String API_SRB = "/srb";
/**
* API Responses.
*/
public static final String API_RBE_MSG = "I Was Sent From a Response Body Emitter!";
public static final String API_SSE_MSG = "I Was Sent From a Sse!";
public static final String API_SRB_MSG = "I Was Sent From a Streaming Response Body!";
}

View File

@@ -0,0 +1,13 @@
package com.baeldung;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
@SpringBootApplication( exclude = SecurityAutoConfiguration.class)
public class Spring5Application {
public static void main(String[] args) {
SpringApplication.run(Spring5Application.class, args);
}
}

View File

@@ -0,0 +1,12 @@
package com.baeldung.html;
import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
@SpringBootApplication
public class HtmlApplication
{
public static void main(String[] args) {
SpringApplication.run(HtmlApplication.class, args);
}
}

View File

@@ -0,0 +1,17 @@
package com.baeldung.html;
import org.springframework.http.*;
import org.springframework.stereotype.*;
import org.springframework.web.bind.annotation.*;
@Controller
public class HtmlController
{
@GetMapping(value = "/welcome", produces = MediaType.TEXT_HTML_VALUE)
@ResponseBody
public String welcomeAsHTML()
{
return "<html>\n" + "<header><title>Welcome</title></header>\n" +
"<body>\n" + "Hello world\n" + "</body>\n" + "</html>";
}
}

View File

@@ -0,0 +1,13 @@
package com.baeldung.idc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,55 @@
package com.baeldung.idc;
import org.springframework.stereotype.Component;
@Component
public class Book {
private final int id;
private final String title;
private final String author;
private final String genre;
public Book() {
this(-1, "", "", "");
}
public Book(int id, String title, String author, String genre) {
this.id = id;
this.title = title;
this.author = author;
this.genre = genre;
}
/**
* @return the id
*/
public int getId() {
return id;
}
/**
* @return the title
*/
public String getTitle() {
return title;
}
/**
* @return the author
*/
public String getAuthor() {
return author;
}
/**
* @return the genre
*/
public String getGenre() {
return genre;
}
}

View File

@@ -0,0 +1,34 @@
package com.baeldung.idc;
import java.util.List;
import java.util.Optional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/book")
public class BookController implements BookOperations {
private BookRepository repo;
public BookController(BookRepository repo) {
this.repo = repo;
}
@Override
public List<Book> getAll() {
return repo.getItems();
}
@Override
public Optional<Book> getById(int id) {
return repo.getById(id);
}
@Override
public void save(Book book, int id) {
repo.save(id, book);
}
}

View File

@@ -0,0 +1,23 @@
package com.baeldung.idc;
import java.util.List;
import java.util.Optional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/default")
public interface BookOperations {
@GetMapping("/")
List<Book> getAll();
@GetMapping("/{id}")
Optional<Book> getById(@PathVariable int id);
@PostMapping("/save/{id}")
public void save(@RequestBody Book book, @PathVariable int id);
}

View File

@@ -0,0 +1,61 @@
package com.baeldung.idc;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import com.github.javafaker.Faker;
/**
* Repository for storing the books.
*
* It serves just for illustrative purposes and is completely in-memory. It uses Java Faker library in order to generate data.
*
* @author A.Shcherbakov
*
*/
@Component
public class BookRepository {
private List<Book> items;
@PostConstruct
public void init() {
Faker faker = new Faker(Locale.ENGLISH);
final com.github.javafaker.Book book = faker.book();
this.items = IntStream.range(1, faker.random()
.nextInt(10, 20))
.mapToObj(i -> new Book(i, book.title(), book.author(), book.genre()))
.collect(Collectors.toList());
}
public int getCount() {
return items.size();
}
public List<Book> getItems() {
return items;
}
public Optional<Book> getById(int id) {
return this.items.stream()
.filter(item -> id == item.getId())
.findFirst();
}
public void save(int id, Book book) {
IntStream.range(0, items.size())
.filter(i -> items.get(i)
.getId() == id)
.findFirst()
.ifPresent(i -> this.items.set(i, book));
}
}

View File

@@ -0,0 +1,82 @@
package com.baeldung.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Foo {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
public Foo() {
super();
}
public Foo(final String name) {
super();
this.name = name;
}
public Foo(final long id, final String name) {
super();
this.id = id;
this.name = name;
}
// API
public String getName() {
return name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public void setName(final String name) {
this.name = name;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Foo other = (Foo) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
return "Foo [name=" + name + "]";
}
}

View File

@@ -0,0 +1,24 @@
package com.baeldung.persistence;
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
import java.util.stream.IntStream;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.baeldung.model.Foo;
@Component
public class DataSetupBean implements InitializingBean {
@Autowired
private FooRepository repo;
@Override
public void afterPropertiesSet() throws Exception {
IntStream.range(1, 5).forEach(i -> repo.save(new Foo(randomAlphabetic(8))));
}
}

View File

@@ -0,0 +1,10 @@
package com.baeldung.persistence;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import com.baeldung.model.Foo;
public interface FooRepository extends JpaRepository<Foo, Long>, JpaSpecificationExecutor<Foo> {
}

View File

@@ -0,0 +1,66 @@
package com.baeldung.web;
import java.util.List;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import com.baeldung.model.Foo;
import com.baeldung.persistence.FooRepository;
@RestController
public class FooController {
@Autowired
private FooRepository repo;
// API - read
@GetMapping("/foos/{id}")
@Validated
public Foo findById(@PathVariable @Min(0) final long id) {
return repo.findById(id).orElse(null);
}
@GetMapping("/foos")
public List<Foo> findAll() {
return repo.findAll();
}
@GetMapping( value="/foos", params = { "page", "size" })
@Validated
public List<Foo> findPaginated(@RequestParam("page") @Min(0) final int page, @Max(100) @RequestParam("size") final int size) {
return repo.findAll(PageRequest.of(page, size)).getContent();
}
// API - write
@PutMapping("/foos/{id}")
@ResponseStatus(HttpStatus.OK)
public Foo update(@PathVariable("id") final String id, @RequestBody final Foo foo) {
return foo;
}
@PostMapping("/foos")
@ResponseStatus(HttpStatus.CREATED)
public void create( @RequestBody final Foo foo) {
if (null == foo || null == foo.getName()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST," 'name' is required");
}
repo.save(foo);
}
}

View File

@@ -0,0 +1,35 @@
package com.baeldung.web;
import com.baeldung.Constants;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
@Controller
public class ResponseBodyEmitterController {
private ExecutorService nonBlockingService = Executors.newCachedThreadPool();
@GetMapping(Constants.API_RBE)
public ResponseEntity<ResponseBodyEmitter> handleRbe() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
nonBlockingService.execute(() -> {
try {
emitter.send(Constants.API_RBE_MSG + " @ " + new Date(), MediaType.TEXT_PLAIN);
emitter.complete();
} catch (Exception ex) {
System.out.println(Constants.GENERIC_EXCEPTION);
emitter.completeWithError(ex);
}
});
return new ResponseEntity(emitter, HttpStatus.OK);
}
}

View File

@@ -0,0 +1,59 @@
package com.baeldung.web;
import java.time.LocalTime;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder;
import com.baeldung.Constants;
@Controller
public class SseEmitterController {
private ExecutorService nonBlockingService = Executors.newCachedThreadPool();
@GetMapping(Constants.API_SSE)
public SseEmitter handleSse() {
SseEmitter emitter = new SseEmitter();
nonBlockingService.execute(() -> {
try {
emitter.send(Constants.API_SSE_MSG + " @ " + new Date());
emitter.complete();
} catch (Exception ex) {
System.out.println(Constants.GENERIC_EXCEPTION);
emitter.completeWithError(ex);
}
});
return emitter;
}
@GetMapping("/stream-sse-mvc")
public SseEmitter streamSseMvc() {
SseEmitter emitter = new SseEmitter();
ExecutorService sseMvcExecutor = Executors.newSingleThreadExecutor();
sseMvcExecutor.execute(() -> {
try {
for (int i = 0; true; i++) {
SseEventBuilder event = SseEmitter.event()
.data("SSE MVC - " + LocalTime.now()
.toString())
.id(String.valueOf(i))
.name("sse event - mvc");
emitter.send(event);
Thread.sleep(1000);
}
} catch (Exception ex) {
emitter.completeWithError(ex);
}
});
return emitter;
}
}

View File

@@ -0,0 +1,23 @@
package com.baeldung.web;
import com.baeldung.Constants;
import java.util.Date;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
@Controller
public class StreamingResponseBodyController {
@GetMapping(Constants.API_SRB)
public ResponseEntity<StreamingResponseBody> handleRbe() {
StreamingResponseBody stream = out -> {
String msg = Constants.API_SRB_MSG + " @ " + new Date();
out.write(msg.getBytes());
};
return new ResponseEntity(stream, HttpStatus.OK);
}
}

View File

@@ -0,0 +1,3 @@
server.port=8081
logging.level.root=INFO

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@@ -0,0 +1,86 @@
<%@taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1" %>
<c:set var="ctx" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Spring MVC Async</title>
<link href="<c:url value="/resources/styles/style.css"/>" rel="stylesheet">
</head>
<body>
<main>
<h2>Spring MVC Async</h2>
<div id="rbe"></div>
<div id="sse"></div>
<div id="srb"></div>
</main>
</body>
<script>
/**
* AJAX Helpers.
*/
var xhr = function(url) {
return new Promise(function(resolve, reject) {
try {
var xmhr = new XMLHttpRequest();
//Listen for API Response
xmhr.onreadystatechange = function() {
if (xmhr.readyState == XMLHttpRequest.DONE && xmhr.status == 200) return resolve(xmhr.responseText);
};
//Open connection
xmhr.open("GET", url, true);
//Additional headers as needed
//x.withCredentials = true;
//x.setRequestHeader("Accept", "application/json");
//x.setRequestHeader("Content-Type", "text/plain");
//Perform the actual AJAX call
xmhr.send();
} catch (ex) {
reject("Exception: Oh CORS's you've made a mistake!");
}
});
};
/**
* RBE
*/
xhr('http://localhost:8080/rbe').then(function(success){
var el = document.getElementById('rbe');
el.appendChild(document.createTextNode(success));
el.appendChild(document.createElement('br'))
});
/**
* SSE
*/
var sse = new EventSource('http://localhost:8080/sse');
sse.onmessage = function (evt) {
var el = document.getElementById('sse');
el.appendChild(document.createTextNode(evt.data));
el.appendChild(document.createElement('br'))
};
/**
* SRB
*/
xhr('http://localhost:8080/srb').then(function(success){
var el = document.getElementById('srb');
el.appendChild(document.createTextNode(success));
el.appendChild(document.createElement('br'))
});
</script>
</html>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>Spring Functional Application</display-name>
<servlet>
<servlet-name>functional</servlet-name>
<servlet-class>com.baeldung.functional.RootServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>functional</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

View File

@@ -0,0 +1,56 @@
package com.baeldung;
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.springframework.http.MediaType;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.response.Response;
import com.jayway.restassured.specification.RequestSpecification;
public class LiveTest {
private static String APP_ROOT = "http://localhost:8081";
@Test
public void givenUser_whenResourceCreatedWithNullName_then400BadRequest() {
final Response response = givenAuth("user", "pass").contentType(MediaType.APPLICATION_JSON.toString())
.body(resourceWithNullName())
.post(APP_ROOT + "/foos");
assertEquals(400, response.getStatusCode());
}
@Test
public void givenUser_whenResourceCreated_then201Created() {
final Response response = givenAuth("user", "pass").contentType(MediaType.APPLICATION_JSON.toString())
.body(resourceString())
.post(APP_ROOT + "/foos");
assertEquals(201, response.getStatusCode());
}
/*@Test
public void givenUser_whenGetAllFoos_thenOK() {
final Response response = givenAuth("user", "pass").get(APP_ROOT + "/foos");
assertEquals(200, response.getStatusCode());
}*/
//
private final String resourceWithNullName() {
return "{\"name\":null}";
}
private final String resourceString() {
return "{\"name\":\"" + randomAlphabetic(8) + "\"}";
}
private final RequestSpecification givenAuth(String username, String password) {
return RestAssured.given()
.auth()
.preemptive()
.basic(username, password);
}
}

View File

@@ -0,0 +1,16 @@
package com.baeldung;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Spring5Application.class)
public class Spring5ApplicationIntegrationTest {
@Test
public void contextLoads() {
}
}

View File

@@ -0,0 +1,30 @@
package com.baeldung.html;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.test.autoconfigure.web.servlet.*;
import org.springframework.test.web.servlet.*;
import org.springframework.test.web.servlet.request.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(HtmlController.class)
class HtmlControllerUnitTest {
@Autowired
private MockMvc mockMvc;
private final String expectedHtmlResponse =
"<html>\n" + "<header><title>Welcome</title></header>\n" +
"<body>\n" + "Hello world\n" + "</body>\n" + "</html>";
@Test
void whenGETRequestToCorrectURL_thenReturnCorrectWelcomeMessage() throws Exception {
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/welcome"))
.andExpect(status().isOk())
.andReturn();
String resultDOW = result.getResponse().getContentAsString();
assertEquals(expectedHtmlResponse, resultDOW);
}
}