spring boot 打包方式 spring boot 整合mybaits REST services

时间:2021-05-09 14:32:52
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build> Spring Boot Maven Plugin – Spring Boot https://docs.spring.io/spring-boot/docs/2.1.1.RELEASE/maven-plugin/ [root@d java]# mvn clean; mvn compile;mvn package;

[root@d java]# java -jar target/springMybatis-1.0-SNAPSHOT.jar com.neo.HelloApplication

[root@d java]# tree -I target
.
├── pom.xml
└── src
├── main
│   ├── java
│   │   └── com
│   │   └── neo
│   │   ├── controller
│   │   │   └── HelloController.java
│   │   ├── dataAccessObj
│   │   │   └── Student.java
│   │   └── HelloApplication.java
│   └── resources
│   ├── SqlMapConfig.xml
│   └── StudentMapper.xml
└── test
└── java

10 directories, 6 files
[root@d java]#

在spring boot的controller中添加 操作数据库代码

package com.neo.controller;

import com.neo.dataAccessObj.Student;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping; import java.io.IOException;
import java.io.Reader; @RestController
public class HelloController { @RequestMapping("/")
public String index() {
return "Hello Spring Boot 2.0!";
} @RequestMapping("/t123")
public String t123() {
return this.getClass().getName() + Thread.currentThread().getStackTrace()[1].getMethodName();
} @RequestMapping("/testMyBaits")
public String testMyBaits() throws IOException {
String res = this.getClass().getName() + Thread.currentThread().getStackTrace()[1].getMethodName(); Reader reader = Resources.getResourceAsReader("SqlMapConfig.xml");
try {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sqlSessionFactory.openSession();
try {
//Create a new student object
Student student;
for (int i = 0; i < 10; i++) {
student = new Student("Mohammad", "It", 80, i * i, System.currentTimeMillis() + "Mohammad@gmail.com");
//Insert student data
session.insert("Student.insert", student);
System.out.println("record inserted successfully" + Integer.toString(i));
}
session.commit();
} finally {
session.close();
}
} catch (IndexOutOfBoundsException e) {
System.err.println("IndexOutOfBoundsException: " + e.getMessage());
}
return res;
} } Tutorial · Building REST services with Spring https://spring.io/guides/tutorials/bookmarks/ TUTORIAL

Building REST services with Spring

REST has quickly become the de-facto standard for building web services on the web because they’re easy to build and easy to consume.

There’s a much larger discussion to be had about how REST fits in the world of microservices, but - for this tutorial - let’s just look at building RESTful services.

Why REST? REST embraces the precepts of the web, including its architecture, benefits, and everything else. This is no surprise given its author, Roy Fielding, was involved in probably a dozen specs which govern how the web operates.

What benefits? The web and its core protocol, HTTP, provide a stack of features:

  • Suitable actions (GETPOSTPUTDELETE, …​)

  • Caching

  • Redirection and forwarding

  • Security (encryption and authentication)

These are all critical factors on building resilient services. But that is not all. The web is built out of lots of tiny specs, hence it’s been able to evolve easily, without getting bogged down in "standards wars".

Developers are able to draw upon 3rd party toolkits that implement these diverse specs and instantly have both client and server technology at their fingertips.

So building on top of HTTP, REST APIs provide the means to build flexible APIs that can:

  • Support backward compatibility

  • Evolvable APIs

  • Scaleable services

  • Securable services

  • A spectrum of stateless to stateful services

What’s important to realize is that REST, however ubiquitous, is not a standard, per se, but an approach, a style, a set of constraints on your architecture that can help you build web-scale systems. In this tutorial we will use the Spring portfolio to build a RESTful service while leveraging the stackless features of REST.

Getting Started

As we work through this tutorial, we’ll use Spring Boot. Go to Spring Initializr and select the following:

  • Web

  • JPA

  • H2

  • Lombok

Then choose "Generate Project". A .zip will download. Unzip it. Inside you’ll find a simple, Maven-based project including a pom.xml build file (NOTE: You can use Gradle. The examples in this tutorial will be Maven based.)

Spring Boot can work with any IDE. You can use Eclipse, IntelliJ IDEA, Netbeans, etc. The Spring Tool Suite is an open-source, Eclipse-based IDE distribution that provides a superset of the Java EE distribution of Eclipse. It includes features that making working with Spring applications even easier. It is, by no means, required. But consider it if you want that extra oomph for your keystrokes. Here’s a video demonstrating how to get started with STS and Spring Boot. This is a general introduction to familiarize you with the tools.

If you pick up IntelliJ IDEA as your IDE for this tutorial, you have to install lombok plugin. In order to see how we install plugins in IntelliJ IDEA please have a look at managing-plugins. After this you have to ensure that "Enable annotation processing" checkbox is ticked under: Preferences → Compiler → Annotation Processors, as it is described https://*.com/questions/14866765/building-with-lomboks-slf4j-and-intellij-cannot-find-symbol-log

 

The Story so Far…​

Let’s start off with the simplest thing we can construct. In fact, to make it as simple as possible, we can even leave out the concepts of REST. (Later on, we’ll add REST to understand the difference.)

Our example models a simple payroll service that manages the employees of a company. Simply put, you need to store employee objects in an H2 in-memory database, and access them via JPA. This will be wrapped with a Spring MVC layer to access remotely.

nonrest/src/main/java/payroll/Employee.java

package payroll;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id; @Data
@Entity
class Employee { private @Id @GeneratedValue Long id;
private String name;
private String role; Employee(String name, String role) {
this.name = name;
this.role = role;
}
}

Despite being small, this Java class contains much:

  • @Data is a Lombok annotation to create all the getters, setters, equalshash, and toString methods, based on the fields.

  • @Entity is a JPA annotation to make this object ready for storage in a JPA-based data store.

  • idname, and role are the attribute for our domain object, the first being marked with more JPA annotations to indicate it’s the primary key and automatically populated by the JPA provider.

  • a custom constructor is created when we need to create a new instance, but don’t yet have an id.

With this domain object definition, we can now turn to Spring Data JPA to handle the tedious database interactions. Spring Data repositories are interfaces with methods supporting reading, updating, deleting, and creating records against a back end data store. Some repositories also support data paging, and sorting, where appropriate. Spring Data synthesizes implementations based on conventions found in the naming of the methods in the interface.

  There are multiple repository implementations besides JPA. You can use Spring Data MongoDB, Spring Data GemFire, Spring Data Cassandra, etc. For this tutorial, we’ll stick with JPA.

nonrest/src/main/java/payroll/EmployeeRepository.java

package payroll;

import org.springframework.data.jpa.repository.JpaRepository;

interface EmployeeRepository extends JpaRepository<Employee, Long> {

}

This interface extends Spring Data JPA’s JpaRepository, specifying the domain type as Employee and the id type as Long. This interface, though empty on the surface, packs a punch given it supports:

  • Creating new instances

  • Updating existing ones

  • Deleting

  • Finding (one, all, by simple or complex properties)

Spring Data’s repository solution makes it possible to sidestep data store specifics and instead solve a majority of problems using domain-specific terminology.

Believe it or not, this is enough to launch an application! A Spring Boot application is, at a minimum, a public static void main entry-point and the @SpringBootApplicationannotation. This tells Spring Boot to help out, wherever possible.

nonrest/src/main/java/payroll/PayrollApplication.java

package payroll;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication
public class PayrollApplication { public static void main(String... args) {
SpringApplication.run(PayrollApplication.class, args);
}
}

@SpringBootApplication is a meta-annotation that pulls in component scanning, autoconfiguration, and property support. We won’t dive into the details of Spring Boot in this tutorial, but in essence, it will fire up a servlet container and serve up our service.

Nevertheless, an application with no data isn’t very interesting, so let’s preload it. The follow class will get loaded automatically by Spring:

nonrest/src/main/java/payroll/LoadDatabase.java

package payroll;

import lombok.extern.slf4j.Slf4j;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
@Slf4j
class LoadDatabase { @Bean
CommandLineRunner initDatabase(EmployeeRepository repository) {
return args -> {
log.info("Preloading " + repository.save(new Employee("Bilbo Baggins", "burglar")));
log.info("Preloading " + repository.save(new Employee("Frodo Baggins", "thief")));
};
}
}

What happens when it gets loaded?

  • Spring Boot will run ALL CommandLineRunner beans once the application context is loaded.

  • This runner will request a copy of the EmployeeRepository you just created.

  • Using it, it will create two entities and store them.

  • @Slf4j is a Lombok annotation to autocreate an Slf4j-based LoggerFactory as log, allowing us to log these newly created "employees".

Right-click and Run PayRollApplication, and this is what you get:

...
2018-08-09 11:36:26.169 INFO 74611 --- [main] payroll.LoadDatabase : Preloading Employee(id=1, name=Bilbo Baggins, role=burglar)
2018-08-09 11:36:26.174 INFO 74611 --- [main] payroll.LoadDatabase : Preloading Employee(id=2, name=Frodo Baggins, role=thief)
...

This isn’t the whole log, but just the key bits of preloading data. (Indeed, check out the whole console. It’s glorious.)

HTTP is the Platform

To wrap your repository with a web layer, you must turn to Spring MVC. Thanks to Spring Boot, there is little in infrastructure to code. Instead, we can focus on actions:

nonrest/src/main/java/payroll/EmployeeController.java

package payroll;

import java.util.List;

import org.springframework.web.bind.annotation.DeleteMapping;
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.RestController; @RestController
class EmployeeController { private final EmployeeRepository repository; EmployeeController(EmployeeRepository repository) {
this.repository = repository;
} // Aggregate root @GetMapping("/employees")
List<Employee> all() {
return repository.findAll();
} @PostMapping("/employees")
Employee newEmployee(@RequestBody Employee newEmployee) {
return repository.save(newEmployee);
} // Single item @GetMapping("/employees/{id}")
Employee one(@PathVariable Long id) { return repository.findById(id)
.orElseThrow(() -> new EmployeeNotFoundException(id));
} @PutMapping("/employees/{id}")
Employee replaceEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) { return repository.findById(id)
.map(employee -> {
employee.setName(newEmployee.getName());
employee.setRole(newEmployee.getRole());
return repository.save(employee);
})
.orElseGet(() -> {
newEmployee.setId(id);
return repository.save(newEmployee);
});
} @DeleteMapping("/employees/{id}")
void deleteEmployee(@PathVariable Long id) {
repository.deleteById(id);
}
}
  • @RestController indicates that the data returned by each method will be written straight into the response body instead of rendering a template.

  • An EmployeeRepository is injected by constructor into the controller.

  • We have routes for each operations (@GetMapping@PostMapping@PutMapping and @DeleteMapping, corresponding to HTTP GETPOSTPUT, and DELETE calls). (NOTE: It’s useful to read each method and understand what they do.)

  • EmployeeNotFoundException is an exception used to indicate when an employee is looked up but not found.

nonrest/src/main/java/payroll/EmployeeNotFoundException.java

package payroll;

class EmployeeNotFoundException extends RuntimeException {

	EmployeeNotFoundException(Long id) {
super("Could not find employee " + id);
}
}

When an EmployeeNotFoundException is thrown, this extra tidbit of Spring MVC configuration is used to render an HTTP 404:

nonrest/src/main/java/payroll/EmployeeNotFoundAdvice.java

package payroll;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; @ControllerAdvice
class EmployeeNotFoundAdvice { @ResponseBody
@ExceptionHandler(EmployeeNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
String employeeNotFoundHandler(EmployeeNotFoundException ex) {
return ex.getMessage();
}
}
  • @ResponseBody signals that this advice is rendered straight into the response body.

  • @ExceptionHandler configures the advice to only respond if an EmployeeNotFoundException is thrown.

  • @ResponseStatus says to issues an HttpStatus.NOT_FOUND, i.e. an HTTP 404.

  • The body of the advice generates the content. In this case, it gives the message of the exception.

To launch the application, either right-click the public static void main in PayRollApplication and select Run from your IDE, or:

Spring Initializr uses maven wrapper so type this:

$ ./mvnw clean spring-boot:run

Alternatively using your installed maven version type this:

$ mvn clean spring-boot:run

When the app starts, we can immediately interrogate it.

$ curl -v localhost:8080/employees

This will yield:

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /employees HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Thu, 09 Aug 2018 17:58:00 GMT
<
* Connection #0 to host localhost left intact
[{"id":1,"name":"Bilbo Baggins","role":"burglar"},{"id":2,"name":"Frodo Baggins","role":"thief"}]

Here you can see the pre-loaded data, in a compacted format.

If you try and query a user that doesn’t exist…​

$ curl -v localhost:8080/employees/99

You get…​

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /employees/99 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 404
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 26
< Date: Thu, 09 Aug 2018 18:00:56 GMT
<
* Connection #0 to host localhost left intact
Could not find employee 99

This message nicely shows an HTTP 404 error with the custom message Could not find employee 99.

It’s not hard to show the currently coded interactions…​

$ curl -X POST localhost:8080/employees -H 'Content-type:application/json' -d '{"name": "Samwise Gamgee", "role": "gardener"}'

Creates a new Employee record, and then sends the content back to us:

{"id":3,"name":"Samwise Gamgee","role":"gardener"}

You can alter the user:

$ curl -X PUT localhost:8080/employees/3 -H 'Content-type:application/json' -d '{"name": "Samwise Gamgee", "role": "ring bearer"}'

Updates the user:

{"id":3,"name":"Samwise Gamgee","role":"ring bearer"}
  Depending on how you construct your service can have significant impacts. In this situation, replace is a better description than update. For example, if the name was NOT provided, it would instead get nulled out.

And you can delete…​

$ curl -X DELETE localhost:8080/employees/3
$ curl localhost:8080/employees/3
Could not find employee 3

This is all well and good, but do we have RESTful service yet? (If you didn’t catch the hint, the answer is no.)

What’s missing?

What makes something RESTful?

So far, you have a web-based service that handles the core operations involving employee data. But that’s not enough to make things "RESTful".

  • Pretty URLs like /employees/3 aren’t REST.

  • Merely using GETPOST, etc. aren’t REST.

  • Having all the CRUD operations laid out aren’t REST.

In fact, what we have built so far is better described as RPC (Remote Procedure Call). That’s because there is no way to know how to interact with this service. If you published this today, you’d also have to write a document or host a developer’s portal somewhere with all the details.

This statement of Roy Fielding may further lend a clue to the difference between REST and RPC:

I am getting frustrated by the number of people calling any HTTP-based interface a REST API. Today’s example is the SocialSite REST API. That is RPC. It screams RPC. There is so much coupling on display that it should be given an X rating.

What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed?

The side effect of NOT including hypermedia in our representations is that clients MUST hard code URIs to navigate the API. This leads to the same brittle nature that predated the rise of e-commerce on the web. It’s a signal that our JSON output needs a little help.

Introducing Spring HATEOAS, a Spring project aimed at helping you write hypermedia-driven outputs. To upgrade your service to being RESTful, add this to your build:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

This tiny library will give us the constructs to define a RESTful service and then render it in an acceptible format for client consumption.

A critical ingredient to any RESTful service is adding links to relevant operations. To make your controller more RESTful, add links like this:

@GetMapping("/employees/{id}")
Resource<Employee> one(@PathVariable Long id) { Employee employee = repository.findById(id)
.orElseThrow(() -> new EmployeeNotFoundException(id)); return new Resource<>(employee,
linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel(),
linkTo(methodOn(EmployeeController.class).all()).withRel("employees"));
}

This is very similar to what we had before, but a few things have changed:

  • The return type of the method has changed from Employee to Resource<Employee>Resource<T> is a generic container from Spring HATEOAS that includes not only the data but a collection of links.

  • linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel() asks that Spring HATEOAS build a link to the EmployeeController 's one() method, and flag it as a self link.

  • linkTo(methodOn(EmployeeController.class).all()).withRel("employees") asks Spring HATEOAS to build a link to the aggregate root, all(), and call it "employees".

What do we mean by "build a link"? One of Spring HATEOAS’s core types is Link. It includes a URI and a rel (relation). Links are what empower the web. Before the World Wide Web, other document systems would render information or links, but it was the linking of documents WITH data that stitched the web together.

Roy Fielding encourages building APIs with the same techniques that made the web successful, and links are one of them.

If you restart the application and query the employee record of Bilbo, you’ll get a slightly different response than earlier:

RESTful representation of a single employee
{
"id": 1,
"name": "Bilbo Baggins",
"role": "burglar",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}

This decompressed output shows not only the data elements you saw earlier (idname and role), but also a _links entry containing two URIs. This entire document is formatted using HAL.

HAL is a lightweight mediatype that allows encoding not just data but also hypermedia controls, alerting consumers to other parts of the API they can navigate toward. In this case, there is a "self" link (kind of like a this statement in code) along with a link back to the aggregate root.

To make the aggregate root ALSO more RESTful, you want to include top level links while ALSO including any RESTful components within:

@GetMapping("/employees")
Resources<Resource<Employee>> all() { List<Resource<Employee>> employees = repository.findAll().stream()
.map(employee -> new Resource<>(employee,
linkTo(methodOn(EmployeeController.class).one(employee.getId())).withSelfRel(),
linkTo(methodOn(EmployeeController.class).all()).withRel("employees")))
.collect(Collectors.toList()); return new Resources<>(employees,
linkTo(methodOn(EmployeeController.class).all()).withSelfRel());
}

Wow! That method, which used to just be repository.findAll() has grown big! Let’s unpack it.

Resources<> is another Spring HATEOAS container aimed at encapsulating collections. It, too, also lets you include links. Don’t let that first statement slip by. When does "encapsulating collections" mean? Collections of employees?

Not quite.

Since we’re talking REST, it should encapsulate collections of employee resources.

That’s why you fetch all the employees, but then transform them into a list of Resource<Employee> objects. (Thanks Java 8 Stream API!)

If you restart the application and fetch the aggregate root, you can see what this looks like.

RESTful representation of a collection of employee resources
{
"_embedded": {
"employeeList": [
{
"id": 1,
"name": "Bilbo Baggins",
"role": "burglar",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
},
{
"id": 2,
"name": "Frodo Baggins",
"role": "thief",
"_links": {
"self": {
"href": "http://localhost:8080/employees/2"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/employees"
}
}
}

For this aggregate root, which serves up a collection of employee resources, there is a top-level "self" link. The "collection" is listed underneath the "_embedded" section. This is how HAL represents collections.

And each individual member of the collection has their information as well as related links.

What is the point of adding all these links? It makes it possible to evolve REST services over time. Existing links can be maintained while new links are added in the future. Newer clients may take advantage of the new links, while legacy clients can sustain themselves on the old links. This is especially helpful if services get relocated and moved around. As long as the link structure is maintained, clients can STILL find and interact with things.

Simplifying Link Creation

Did you notice the repetition in single employee link creation? The code to provide a single link to an employee as well as an "employees" link to the aggregate root was shown twice. If that raised your concern, good! There’s a solution.

Simply put, you need to define a function that converts Employee objects to Resource<Employee> objects. While you could easily code this method yourself, there are benefits down the road of implementing Spring HATEOAS’s ResourceAssembler interface.

package payroll;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;

import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceAssembler;
import org.springframework.stereotype.Component; @Component
class EmployeeResourceAssembler implements ResourceAssembler<Employee, Resource<Employee>> { @Override
public Resource<Employee> toResource(Employee employee) { return new Resource<>(employee,
linkTo(methodOn(EmployeeController.class).one(employee.getId())).withSelfRel(),
linkTo(methodOn(EmployeeController.class).all()).withRel("employees"));
}
}

This simple interface has one method: toResource(). It is based on converting a non-resource object (Employee) into a resource-based object (Resource<Employee>).

All the code you saw easiler in the controller can be moved into this class. And by applying Spring Framework’s @Component, this component will be automatically created when the starts.

  Spring HATEOAS’s abstract base class for all resources is ResourceSupport. But for simplicity, I recommend using Resource<T> as your mechanism to easily wrap all POJOs as resources.

To leverage this assembler, you only have to alter the EmployeeController by injecting the assembler in the constructor. Then the

@RestController
class EmployeeController { private final EmployeeRepository repository; private final EmployeeResourceAssembler assembler; EmployeeController(EmployeeRepository repository,
EmployeeResourceAssembler assembler) { this.repository = repository;
this.assembler = assembler;
} ... }

From here, you can use it in the single-item employee method:

@GetMapping("/employees/{id}")
Resource<Employee> one(@PathVariable Long id) { Employee employee = repository.findById(id)
.orElseThrow(() -> new EmployeeNotFoundException(id)); return assembler.toResource(employee);
}

This code is almost the same, except instead of creating the Resource<Employee> instance here, you delegate it to the assembler. Maybe that doesn’t look like much?

Applying the same thing in the aggregate root controller method is more impressive:

@GetMapping("/employees")
Resources<Resource<Employee>> all() { List<Resource<Employee>> employees = repository.findAll().stream()
.map(assembler::toResource)
.collect(Collectors.toList()); return new Resources<>(employees,
linkTo(methodOn(EmployeeController.class).all()).withSelfRel());
}

The code is, again, almost the same, however you get to replace all that Resource<Employee>creation logic with map(assembler::toResource). Thanks to Java 8 method references, it’s super easy to plug it in and simplify your controller.

  A key design goal of Spring HATEOAS is to make it easier to do The Right Thing™. In this scenario, adding hypermedia to your service without hard coding anything.

At this stage, you’ve create a Spring MVC REST controller that actually produces hypermedia-powered content! Clients that don’t speak HAL can ignore the extra bits while consuming the pure data. Clients that DO speak HAL can navigate your empowered API.

But that is not the only thing needed to build a truly RESTful service with Spring.

Evolving REST APIs

With one additional library and a few lines of extra code, you have added hypermedia to your application. But that is not the only thing needed to make your service RESTful. An important facet of REST is the fact that it’s neither a technology stack nor a single standard.

REST is a collection of architectural constraints that when adopted make your application much more resilient. A key factor of resilience is that when you make upgrades to your services, your clients don’t suffer from downtime.

In the "olden" days, upgrades were notorious for breaking clients. In other words, an upgrade to the server required an update to the client. In this day and age, hours or even minutes of downtime spent doing an upgrade can cost millions in lost revenue.

Some companies require that you present management with a plan to minimize downtime. In the past, you could get away with upgrading at 2:00 a.m. on a Sunday when load was at a minimum. But in today’s Internet-based e-commerce with international customers, such strategies are not as effective.

SOAP-based services and CORBA-based services were incredibly brittle. It was hard to roll out a server that could support both old and new clients. With REST-based practices, it’s much easier. Especially using the Spring stack.

Imagine this design problem: You’ve rolled out a system with this Employee -based record. The system is a major hit. You’ve sold your system to countless enterprises. Suddenly, the need for an employee’s name to be split into firstName and lastName arises.

Uh oh. Didn’t think of that.

Before you open up the Employee class and replace the single field name with firstNameand lastName, stop and think for a second. Will that break any clients? How long will it take to upgrade them. Do you even control all the clients accessing your services?

Downtime = lost money. Is management ready for that?

There is an old strategy that precedes REST by years.

Never delete a column in a database.

— Unknown

You can always add columns (fields) to a database table. But don’t take one away. The principle in RESTful services is the same. Add new fields to your JSON representations, but don’t take any away. Like this:

{
"id": 1,
"firstName": "Bilbo",
"lastName": "Baggins",
"role": "burglar",
"name": "Bilbo Baggins",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}

Notice how this format shows firstNamelastName, AND name? While it sports duplication of information, the purpose is to support both old and new clients. That means you can upgrade the server without requiring clients upgrade at the same time. A good move that should reduce downtime.

And not only should you show this information in both the "old way" and the "new way". You should also process incoming data both ways.

How? Simple. Like this:

Employee record that handles both "old" and "new" clients
package payroll;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id; @Data
@Entity
class Employee { private @Id @GeneratedValue Long id;
private String firstName;
private String lastName;
private String role; Employee(String firstName, String lastName, String role) {
this.firstName = firstName;
this.lastName = lastName;
this.role = role;
} public String getName() {
return this.firstName + " " + this.lastName;
} public void setName(String name) {
String[] parts =name.split(" ");
this.firstName = parts[0];
this.lastName = parts[1];
}
}

This class is very similar to the previous version of Employee. Let’s go over the changes:

  • Field name has been replaced by firstName and lastName. Lombok will generate getters and setters for those.

  • A "virtual" getter for the old name property, getName() is defined. It uses the firstNameand lastName fields to produce a value.

  • A "virtual" setter for the old name property is also defined, setName(). It parses an incoming string and stores it into the proper fields.

Of course not EVERY change to your API is as simple as splitting a string or merging two strings. But it’s surely not impossible to come up with a set of transforms for most scenarios, ehh?

Another fine tuning is to ensure each of your REST methods returns a proper response. Update the POST method like this:

@PostMapping("/employees")
ResponseEntity<?> newEmployee(@RequestBody Employee newEmployee) throws URISyntaxException { Resource<Employee> resource = assembler.toResource(repository.save(newEmployee)); return ResponseEntity
.created(new URI(resource.getId().expand().getHref()))
.body(resource);
}
  • The new Employee object is saved as before. But the resulting object is wrapped using the EmployeeResourceAssembler.

  • Spring MVC’s ResponseEntity is used to create an HTTP 201 Created status message. This type of response typically includes a Location response header, and we use the newly formed link.

  • Additionally, return the resource-based version of the saved object.

With this tweak in place, you can use the same endpoint to create a new employee resource, and use the legacy name field:

$ curl -v -X POST localhost:8080/employees -H 'Content-Type:application/json' -d '{"name": "Samwise Gamgee", "role": "gardener"}'

The output is shown below:

> POST /employees HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type:application/json
> Content-Length: 46
>
< Location: http://localhost:8080/employees/3
< Content-Type: application/hal+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Fri, 10 Aug 2018 19:44:43 GMT
<
{
"id": 3,
"firstName": "Samwise",
"lastName": "Gamgee",
"role": "gardener",
"name": "Samwise Gamgee",
"_links": {
"self": {
"href": "http://localhost:8080/employees/3"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}

This not only has the resulting object rendered in HAL (both name as well as firstName/lastName), but also the Location header populated with http://localhost:8080/employees/3. A hypermedia powered client could opt to "surf" to this new resource and proceed to interact with it.

The PUT controller method needs similar tweaks:

@PutMapping("/employees/{id}")
ResponseEntity<?> replaceEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) throws URISyntaxException { Employee updatedEmployee = repository.findById(id)
.map(employee -> {
employee.setName(newEmployee.getName());
employee.setRole(newEmployee.getRole());
return repository.save(employee);
})
.orElseGet(() -> {
newEmployee.setId(id);
return repository.save(newEmployee);
}); Resource<Employee> resource = assembler.toResource(updatedEmployee); return ResponseEntity
.created(new URI(resource.getId().expand().getHref()))
.body(resource);
}

The Employee object built from the save() operation is then wrapped using the EmployeeResourceAssembler into a Resource<Employee> object. Since we want a more detailed HTTP response code than 200 OK, we will use Spring MVC’s ResponseEntitywrapper. It has a handy static method created() where we can plug in the resource’s URI.

By grabbing the resource you can fetch it’s "self" link via the getId() method call. This method yields a Link which you can turn into a Java URI. To tie things up nicely, you inject the resource itself into the body() method.

  In REST, a resource’s id is the URI of that resource. Hence, Spring HATEOAS doesn’t hand you the id field of the underlying data type (which no client should), but instead, the URI for it. And don’t confuse ResourceSupport.getId() with Employee.getId().

It’s debatable if HTTP 201 Created carries the right semantics since we aren’t necessarily "creating" a new resource. But it comes pre-loaded with a Location response header, so run with it.

$ curl -v -X PUT localhost:8080/employees/3 -H 'Content-Type:application/json' -d '{"name": "Samwise Gamgee", "role": "ring bearer"}'

* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> PUT /employees/3 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type:application/json
> Content-Length: 49
>
< HTTP/1.1 201
< Location: http://localhost:8080/employees/3
< Content-Type: application/hal+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Fri, 10 Aug 2018 19:52:56 GMT
{
"id": 3,
"firstName": "Samwise",
"lastName": "Gamgee",
"role": "ring bearer",
"name": "Samwise Gamgee",
"_links": {
"self": {
"href": "http://localhost:8080/employees/3"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}

That employee resource has now been updated and the location URI sent back. Finally, update the DELETE operation suitably:

@DeleteMapping("/employees/{id}")
ResponseEntity<?> deleteEmployee(@PathVariable Long id) { repository.deleteById(id); return ResponseEntity.noContent().build();
}

This returns an HTTP 204 No Content response.

$ curl -v -X DELETE localhost:8080/employees/1

* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> DELETE /employees/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 204
< Date: Fri, 10 Aug 2018 21:30:26 GMT
  Making changes to the fields in the Employee class will require coordination with your database team, so that they can properly migrate existing content into the new columns.

You are now ready for an upgrade that will NOT disturb existing clients while newer clients can take advantage of the enhancements!

By the way, are you worried about sending too much information over the wire? In some systems where every byte counts, evolution of APIs may need to take a backseat. But don’t pursue such premature optimization until until you measure.

Building links into your REST API

So far, you’ve built an evolvable API with bare bones links. To grow your API and better serve your clients, you need to embrace the concept of Hypermedia as the Engine of Application State.

What does that mean? In this section, you’ll explore it in detail.

Business logic inevitably builds up rules that involve processes. The risk of such systems if we often carry such server-side logic into clients and build up strong coupling. REST is about breaking down such connections and minimizing such coupling.

To show how to cope with state changes without triggering breaking changes in clients, imagine adding a system that fulfills orders.

As a first step, define an Order record:

package payroll;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table; @Entity
@Data
@Table(name = "CUSTOMER_ORDER")
class Order { private @Id @GeneratedValue Long id; private String description;
private Status status; Order(String description, Status status) { this.description = description;
this.status = status;
}
}
  • The class requires a JPA @Table annotation changing the table’s name to CUSTOMER_ORDER because ORDER is not a valid name for table.

  • It includes a description field as well as a status field.

Orders must go through a certain series of state transitions from the time a customer submits an order and it is either fulfilled or cancelled. This can be captured as a Java enum:

package payroll;

enum Status {

	IN_PROGRESS,
COMPLETED,
CANCELLED;
}

This enum captures the various states an Order can occupy. For this tutorial, let’s keep it simple.

To support interacting with orders in the database, you must define a corresponding Spring Data repository:

interface OrderRepository extends JpaRepository<Order, Long> {
}

With this in place, you can now define a basic OrderController:

@RestController
class OrderController { private final OrderRepository orderRepository;
private final OrderResourceAssembler assembler; OrderController(OrderRepository orderRepository,
OrderResourceAssembler assembler) { this.orderRepository = orderRepository;
this.assembler = assembler;
} @GetMapping("/orders")
Resources<Resource<Order>> all() { List<Resource<Order>> orders = orderRepository.findAll().stream()
.map(assembler::toResource)
.collect(Collectors.toList()); return new Resources<>(orders,
linkTo(methodOn(OrderController.class).all()).withSelfRel());
} @GetMapping("/orders/{id}")
Resource<Order> one(@PathVariable Long id) {
return assembler.toResource(
orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id)));
} @PostMapping("/orders")
ResponseEntity<Resource<Order>> newOrder(@RequestBody Order order) { order.setStatus(Status.IN_PROGRESS);
Order newOrder = orderRepository.save(order); return ResponseEntity
.created(linkTo(methodOn(OrderController.class).one(newOrder.getId())).toUri())
.body(assembler.toResource(newOrder));
}
}
  • It contains the same REST controller setup as the controllers you’ve built so far.

  • It injects both an OrderRepository as well as a (not yet built) OrderResourceAssembler.

  • The first two Spring MVC routes handle the aggregate root as well as a single item Orderresource request.

  • The third Spring MVC route handles creating new orders, by starting them in the IN_PROGRESS state.

  • All the controller methods return one of Spring HATEOAS’s ResourceSupport subclasses to properly render hypermedia (or an wrapper around such a type).

Before building the OrderResourceAssembler, let’s discuss what needs to happen. You are modeling the flow of states between Status.IN_PROGRESSStatus.COMPLETED, and Status.CANCELLED. A natural thing when serving up such data to clients is to let the clients make the decision on what it can do based on this payload.

But that would be wrong.

What happens when you introduce a new state in this flow? The placement of various buttons on the UI would probably be erroneous.

What if you changed the name of each state, perhaps while coding international support and showing locale-specific text for each state? That would most likely break all the clients.

Enter HATEOAS or Hypermedia as the Engine of Application State. Instead of clients parsing the payload, give them links to signal valid actions. Decouple state-based actions from the payload of data. In other words, when CANCEL and COMPLETE are valid actions, dynamically add them to the list of links. Clients only need show users the corresponding buttons when the links exist.

This decouples clients from having to know WHEN such actions are valid, reducing the risk of the server and its clients getting out of sync on the logic of state transitions.

Having already embraced the concept of Spring HATEOAS ResourceAssembler components, putting such logic in the OrderResourceAssembler would be the perfect place to capture this business rule:

package payroll;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;

import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceAssembler;
import org.springframework.stereotype.Component; @Component
class OrderResourceAssembler implements ResourceAssembler<Order, Resource<Order>> { @Override
public Resource<Order> toResource(Order order) { // Unconditional links to single-item resource and aggregate root Resource<Order> orderResource = new Resource<>(order,
linkTo(methodOn(OrderController.class).one(order.getId())).withSelfRel(),
linkTo(methodOn(OrderController.class).all()).withRel("orders")
); // Conditional links based on state of the order if (order.getStatus() == Status.IN_PROGRESS) {
orderResource.add(
linkTo(methodOn(OrderController.class)
.cancel(order.getId())).withRel("cancel"));
orderResource.add(
linkTo(methodOn(OrderController.class)
.complete(order.getId())).withRel("complete"));
} return orderResource;
}
}

This resource assembler always includes the self link to the single-item resource as well as a link back to the aggregate root. But it also includes two conditional links to OrderController.cancel(id) as well as OrderController.complete(id) (not yet defined). These links are ONLY shown when the order’s status is Status.IN_PROGRESS.

If clients can adopt HAL and the ability to read links instead of simply reading the data of plain old JSON, they can trade in the need for domain knowledge about the order system. This naturally reduces coupling between client and server. And it opens the door to tuning the flow of order fulfillment without breaking clients in the process.

To round out order fulfillment, add the following to the OrderController for the canceloperation:

@DeleteMapping("/orders/{id}/cancel")
ResponseEntity<ResourceSupport> cancel(@PathVariable Long id) { Order order = orderRepository.findById(id).orElseThrow(() -> new OrderNotFoundException(id)); if (order.getStatus() == Status.IN_PROGRESS) {
order.setStatus(Status.CANCELLED);
return ResponseEntity.ok(assembler.toResource(orderRepository.save(order)));
} return ResponseEntity
.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(new VndErrors.VndError("Method not allowed", "You can't cancel an order that is in the " + order.getStatus() + " status"));
}

It checks the Order status before allowing it to be cancelled. If it’s not a valid state, it returns a Spring HATEOAS VndError, a hypermedia-supporting error container. If the transition is indeed valid, it transitions the Order to CANCELLED.

And add this to the OrderController as well for order completion:

@PutMapping("/orders/{id}/complete")
ResponseEntity<ResourceSupport> complete(@PathVariable Long id) { Order order = orderRepository.findById(id).orElseThrow(() -> new OrderNotFoundException(id)); if (order.getStatus() == Status.IN_PROGRESS) {
order.setStatus(Status.COMPLETED);
return ResponseEntity.ok(assembler.toResource(orderRepository.save(order)));
} return ResponseEntity
.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(new VndErrors.VndError("Method not allowed", "You can't complete an order that is in the " + order.getStatus() + " status"));
}

This implements similar logic to prevent an Order status from being completed unless in the proper state.

By adding a little extra initialization code to LoadDatabase:

orderRepository.save(new Order("MacBook Pro", Status.COMPLETED));
orderRepository.save(new Order("iPhone", Status.IN_PROGRESS)); orderRepository.findAll().forEach(order -> {
log.info("Preloaded " + order);
});

…​you can test things out!

To use the newly minted order service, just perform a few operations:

$ curl -v http://localhost:8080/orders

{
"_embedded": {
"orderList": [
{
"id": 3,
"description": "MacBook Pro",
"status": "COMPLETED",
"_links": {
"self": {
"href": "http://localhost:8080/orders/3"
},
"orders": {
"href": "http://localhost:8080/orders"
}
}
},
{
"id": 4,
"description": "iPhone",
"status": "IN_PROGRESS",
"_links": {
"self": {
"href": "http://localhost:8080/orders/4"
},
"orders": {
"href": "http://localhost:8080/orders"
},
"cancel": {
"href": "http://localhost:8080/orders/4/cancel"
},
"complete": {
"href": "http://localhost:8080/orders/4/complete"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/orders"
}
}
}

This HAL document immediately shows different links for each order, based upon its present state.

  • The first order, being COMPLETED only has the navigational links. The state transition links are not shown.

  • The second order, being IN_PROGRESS additionally has the cancel link as well as the complete link.

Try cancelling an order:

$ curl -v -X DELETE http://localhost:8080/orders/4/cancel

> DELETE /orders/4/cancel HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200
< Content-Type: application/hal+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Mon, 27 Aug 2018 15:02:10 GMT
<
{
"id": 4,
"description": "iPhone",
"status": "CANCELLED",
"_links": {
"self": {
"href": "http://localhost:8080/orders/4"
},
"orders": {
"href": "http://localhost:8080/orders"
}
}
}

This response shows an HTTP 200 status code indicating it was successful. The response HAL document shows that order in its new state (CANCELLED). And the state-altering links gone.

If you try the same operate again…​

$ curl -v -X DELETE http://localhost:8080/orders/4/cancel

* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> DELETE /orders/4/cancel HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 405
< Content-Type: application/hal+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Mon, 27 Aug 2018 15:03:24 GMT
<
{
"logref": "Method not allowed",
"message": "You can't cancel an order that is in the CANCELLED status"
}

…​you see an HTTP 405 Method Not Allowed response. DELETE has become an invalid operation. The VndError response object clearly indicates that you aren’t allowed to "cancel" an order already in the "CANCELLED" status.

Additionally, trying to complete the same order also fails:

$ curl -v -X PUT localhost:8080/orders/4/complete

* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> PUT /orders/4/complete HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 405
< Content-Type: application/hal+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Mon, 27 Aug 2018 15:05:40 GMT
<
{
"logref": "Method not allowed",
"message": "You can't complete an order that is in the CANCELLED status"
}

With all this in place, your order fulfillment service is capable of conditionally showing what operations are available. It also guards against invalid operations.

By leveraging the protocol of hypermedia and links, clients can be built sturdier and less likely to break simply because of a change in the data. And Spring HATEOAS eases building the hypermedia you need to serve to your clients.

Summary

Throughout this tutorial, you have engaged in various tactics to build REST API. As it turns out, REST isn’t just about pretty URIs and returning JSON instead of XML.

Instead, the following tactics help make your services less likely to break existing clients you may or may not control:

  • Don’t remove old fields. Instead, support them.

  • Use rel-based links so clients don’t have to hard code URIs.

  • Retain old links as long as possible. Even if you have to change the URI, keep the rels so older clients have a path onto the newer features.

  • Use links, not payload data, to instruct clients when various state-driving operations are available.

It may appear to be a bit of effort to build up ResourceAssembler implementations for each resource type and to use these components in all of your controllers. But this extra bit of server-side setup (made easy thanks to Spring HATEOAS) can ensure the clients you control (and more importantly, those you don’t) can upgrade with ease as you evolve your API.

This concludes our tutorial on how to build RESTful services using Spring. Each section of this tutorial is managed as a separate subproject in a single github repo:

  • nonrest - Simple Spring MVC app with no hypermedia

  • rest - Spring MVC + Spring HATEOAS app with HAL representations of each resource

  • evolution - REST app where a field is evolved but old data is retained for backward compatibility

  • links - REST app where conditional links are used to signal valid state changes to clients

To view more examples of using Spring HATEOAS see https://github.com/spring-projects/spring-hateoas-examples.

To do some more exploring check out the following video by Spring teammate Oliver Gierke:

 

Want to write a new guide or contribute to an existing one? Check out our contribution guidelines.

http://ond:8080/getSingleStudent/2

com.neo.controller.HelloControllergetSingleStudentJim1543995796678Mohammad@gmail.comIt

com.neo.controller.HelloController

@GetMapping("/getSingleStudent/{encryptedId}")
public String getSingleStudent(@PathVariable int encryptedId) throws IOException {
String res = this.getClass().getName() + Thread.currentThread().getStackTrace()[1].getMethodName();
Reader reader = Resources.getResourceAsReader("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sqlSessionFactory.openSession();
try {
Student student = (Student) session.selectOne("Student.getStudentById", encryptedId);
session.commit();
res += student.getName() + student.getEmail()+student.getBranch();
} finally {
session.close();
}
return res;
}

src\main\resources\StudentMapper.xml
<select id="getStudentById" parameterType="Student" resultMap="result">

    SELECT * FROM STUDENT WHERE id = #{id}

</select>

返回json格式字符串
import com.alibaba.fastjson.JSONObject;
@GetMapping("/getSingleStudent/{encryptedId}")
public ResponseEntity getSingleStudent(@PathVariable int encryptedId) throws IOException {
String res = this.getClass().getName() + Thread.currentThread().getStackTrace()[1].getMethodName();
Reader reader = Resources.getResourceAsReader("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sqlSessionFactory.openSession();
try {
Student student = (Student) session.selectOne("Student.getStudentById", encryptedId);
session.commit();
res += student.getName() + student.getEmail() + student.getBranch();
} finally {
session.close();
}
JSONObject jsonObject = JSONObject.parseObject("{\"totalPv\":\"123\"}");
return new ResponseEntity<JSONObject>(jsonObject, HttpStatus.OK);
}