欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

程序员文章站 2022-07-12 20:31:32
...

REST


即表述性状态转移(REpresentational State Transfer),是一种基于HTTP的结构原则,一种表示被操作的资源的方法。
REST Web服务完全依赖HTTP方法,每一种方法都会对某一种资源进行操作。GET方法常用来获取某一资源或者资源集合。POST方法则用于创建。PUT方法用于更新。DELETE方法用于从系统中删除资源。

使用REST Web服务进行CRUD操作


本章所用的依赖文件有:

<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>
  <groupId>org.springframework.samples.service.service</groupId>
  <artifactId>SpringAOPTest</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>

    <properties>

        <!-- Generic properties -->
        <java.version>1.6</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

        <!-- Web -->
        <jsp.version>2.3.1</jsp.version>
        <jstl.version>1.2</jstl.version>
        <servlet.version>3.1.0</servlet.version>


        <!-- Spring -->
        <spring-framework.version>4.3.10.RELEASE</spring-framework.version>

        <!-- Hibernate / JPA -->
        <hibernate.version>5.2.10.Final</hibernate.version>

        <!-- Logging -->
        <logback.version>1.2.3</logback.version>
        <slf4j.version>1.7.25</slf4j.version>

        <!-- Test -->
        <junit.version>4.12</junit.version>

        <!-- AspectJ -->
        <aspectj.version>1.8.10</aspectj.version>

        <!-- JSON Evaluation -->
        <jackson.version>2.9.0</jackson.version>

    </properties>

    <dependencies>

        <!-- Spring MVC -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring-framework.version}</version>
        </dependency>

        <!-- Other Web dependencies -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>${jstl.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>${servlet.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>${jsp.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- Spring and Transactions -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring-framework.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>${spring-framework.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring-framework.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring-framework.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring-framework.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring-framework.version}</version>
        </dependency>

        <!-- Logging with SLF4J & LogBack -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
            <scope>runtime</scope>
        </dependency>

        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>${hibernate.version}</version>
        </dependency>


        <!-- Test Artifacts -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring-framework.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- AspectJ -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${aspectj.version}</version>
        </dependency>

        <!-- JSON Evaluation -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>

    </dependencies> 
</project>

项目目录结构如下:
Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

首先在com.lonelyquantum.wileybookch11.domain包中创建域类User:

public class User {

    private int id;
    private String name;

    public User() {}

    public User(int id, String name){
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

然后去com.lonelyquantum.wileybookch11.repository包中创建文件UserRepository类存储User对象。

@Repository
public class UserRepository {

    private Map<Integer, User> users = new HashMap<Integer, User>();

    @PostConstruct
    public void setup(){
        users.put(1, new User(1,"Madoka Kaname"));
        users.put(2, new User(2,"Homura Akemi"));
    }

    public void save(User user){
        users.put(user.getId(), user);
    }

    public List<User> findAll(){
        return new ArrayList<User>(users.values());
    }

    public User find(int id){
        return users.get(id);
    }

    public void update(int id, User user){
        users.put(id, user);
    }

    public void delete(int id){
        users.remove(id);
    }
}

该类通过Map来存储Id,UserName对,并提供了CRUD方法。
接下来在com.lonelyquantum.wileybookch11.controller包中创建UserRestController类进行控制。

@RestController
@RequestMapping("/rest")
public class UserRestController {

    @Autowired
    private UserRepository userRepository;

    @RequestMapping(value = "/users", method = RequestMethod.POST)
    public void save(@RequestBody User user){
        userRepository.save(user);
    }

    @RequestMapping(value = "/users", method = RequestMethod.GET)
    public List<User> list(){
        return userRepository.findAll();
    }

    @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
    public User get(@PathVariable("id") int id){
        User user = userRepository.find(id);
        if(user == null){
            throw new RestException(1, "User not found!", "User with id: " + id + " not found in the system");
        }
        return user;
    }

    @RequestMapping(value = "/users/{id}", method = RequestMethod.PUT)
    public void update(@PathVariable("id") int id, @RequestBody User user){
        userRepository.update(id, user);
    }

    @RequestMapping(value = "/users/{id}", method = RequestMethod.DELETE)
    public ResponseEntity<Boolean> delete(@PathVariable("id") int id){
        userRepository.delete(id);
        return new ResponseEntity<Boolean>(Boolean.TRUE, HttpStatus.OK);
    }
}

该类将Web请求映射到处理方法上。
接着采用springmvc-servlet.xml来配置servlet。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context-4.0.xsd
                           http://www.springframework.org/schema/mvc
                           http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

    <context:component-scan base-package="com.lonelyquantum.wileybookch11" />
    <context:annotation-config />

    <mvc:annotation-driven />
</beans>

当然,也可以通过在包com.lonelyquantum.wileybookch11.config中创建配置类来配置Servlet:

@Configuration
@ComponentScan(basePackages = {"com.lonelyquantum.wileybookch11"})
@EnableWebMvc
public class AppConfig {

}

然后在web.xml中用URL定义DispatcherServlet:

 <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">

    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

如果是采用Java配置的servlet则应该在配置中引用AppConfig类。

 <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">

     <servlet>
         <servlet-name>springmvc</servlet-name>
         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
         <init-param>
             <param-name>contextClass</param-name>
             <param-value>
                 org.springframework.web.context.support.AnnotationConfigWebApplicationContext
             </param-value>
         </init-param>
         <init-param>
             <param-name>contextConfigLocation</param-name>
             <param-value>
                 com.lonelyquantum.wileybookch11.config.AppConfig
             </param-value>
         </init-param>
         <load-on-startup>1</load-on-startup>
     </servlet>

     <servlet-mapping>
         <servlet-name>springmvc</servlet-name>
         <url-pattern>/*</url-pattern>
     </servlet-mapping>
 </web-app>

将工程部署到Tomcat上运行容器,就可以用SoapUI进行REST调试了。
打开SoapUI后首先File->NEW REST Project

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

如上填写url。
然后生成如下项目:

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

之后点击左上角运行图标,将右侧选项卡调整至JSON。可得返回结果:

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

此处Method为GET,表示采用的是GET方法。在user路径采用该方法返回的是全部用户的User类。
可以右键资源添加新方法以测试其他操作:

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

我们可以先建立一个名为New User的POST方法来创建新用户:

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

为了使用这个方法,我们需要输入新用户的参数再执行:

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

注意输入的数据类型调整正确。左下角会显示相应时间,该方法没有返回值,但是可以再次执行之前的GET方法显示插入的用户。

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

接下来创建Update方法,采用的是PUT方法,注意,该方法的资源需要指向具体的user,输入类型也应该调整为json。

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

执行完毕再使用第一个GET方法可以看到user3的内容已经更新:

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

最后创建DELETE 方法,该方法与UPDATE类似,也需要指定具体user。

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

执行完毕再用第一个GET方法查看,可发现id=3的user已经被删除。

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

可以从方法中返回HTTP状态码告知用户方法执行情况。之前delete方法中就返回了return new ResponseEntity<Boolean>(Boolean.TRUE, HttpStatus.OK);表示成功。状态码如下:
1xx 信息
2xx 成功
3xx 重定向
4xx 客户端错误
5xx 服务器错误

使用XML响应的REST Web服务


刚刚的项目中我们的REST方法返回的都是JSON格式的数据,其实也可以用XML格式返回数据。只要给域类添加如**解:

@XmlRootElement
public class User {

    @XmlElement
    private int id;
    @XmlElement
    private String name;

    public User() {}

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

就可以在查找某个user时返回XML格式的输出
Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

使用异常处理机制


先在com.lonelyquantum.wileybookch11.domain创建RestErrorMessage类来表示异常信息。

public class RestErrorMessage {

    private HttpStatus status;
    private int code;
    private String message;
    private String detailedMessage;
    private String exceptionMessage;

    public RestErrorMessage(HttpStatus status, int code, String message, String detailedMessage,
            String exceptionMessage) {
        super();
        this.status = status;
        this.code = code;
        this.message = message;
        this.detailedMessage = detailedMessage;
        this.exceptionMessage = exceptionMessage;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public String getDetailedMessage() {
        return detailedMessage;
    }

    public String getExceptionMessage() {
        return exceptionMessage;
    }

}

在该项目中创建com.lonelyquantum.wileybookch11.exception包来保存抛出的异常

public class RestException extends RuntimeException{

    private int code;
    private String message;
    private String detailedMessage;

    public RestException(int code, String message, String detailedMessage){
        this.code = code;
        this.message = message;
        this.detailedMessage = detailedMessage;
    }

    public int getCode() {
        return code;
    }

    @Override
    public String getMessage() {
        // TODO Auto-generated method stub
        return super.getMessage();
    }

    public String getDetailedMessage() {
        return detailedMessage;
    }
}

以及com.lonelyquantum.wileybookch11.handler包来保存处理异常的方法。

@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler{

    @ExceptionHandler(Exception.class)
    protected ResponseEntity<Object> handleInvalidRequest(RestException e, ServletWebRequest request){
        RestErrorMessage error = new RestErrorMessage(HttpStatus.valueOf(request.getResponse().getStatus()), 
                e.getCode(), e.getMessage(), e.getDetailedMessage(), e.toString());

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        return handleExceptionInternal(e, error, headers, HttpStatus.OK, request);
    }
}

Controller中的异常处理之前已经写好。
这时我们如果试图查找已经被删除的id=3的user就会返回如下错误信息。

Beginning Spring学习笔记——第11章 使用Spring开发REST风格的Web服务

对REST风格服务进行单元测试


通过Spring提供的RestTemplate模板类,我们可以通过测试代码来情锁模拟HTTP请求。二者映射名称如下:

  • GET:getForObject(string, Class,String…)
  • PUT: put(String, Object, String)
  • POST:postForLocation(String, Object, String…)
  • DELETE:delete(String, String)
  • HEAD:headForHeaders(String,String)
  • OPTIONS:optionsForAllow(String, String)

可以在/src/test/java目录下创建com.lonelyquantum.wileybookch11包并在其中创建如下测试类用于测试各个方法:

public class AddUserTest {
    @Test
    public void addUserWorksOK(){
        RestTemplate template = new RestTemplate();
        User user = new User(3,"Sayaka Miki");
        ResponseEntity<Void> resultSave = template.postForEntity("http://localhost:8080/SpringREST/rest/users", user, Void.class);
        assertNotNull(resultSave);
    }
}

public class DeleteUserTest {

    @Test
    public void deleteUserWorksOK(){
        RestTemplate template = new RestTemplate();
        template.delete("http://localhost:8080/SpringREST/rest/users/3");

        ResponseEntity<List> resultList = template.getForEntity("http://localhost:8080/SpringREST/rest/users", List.class);
        assertNotNull(resultList);
        assertNotNull(resultList.getBody());
        assertThat(resultList.getBody().size(), is(2));
    }
}

public class ListUsersTest {
    @Test
    public void listUserWorksOK(){
        RestTemplate template = new RestTemplate();
        ResponseEntity<List> result = template.getForEntity("http://localhost:8080/SpringREST/rest/users", List.class);
        assertNotNull(result);
        assertNotNull(result.getBody());
        assertThat(result.getBody().size(), is(2));
    }
}

public class UpdateUserTest {
    @Test
    public void updateUserWorksOK(){
        RestTemplate template = new RestTemplate();
        User user = new User(3, "Sakura Kyouko");
        template.put("http://localhost:8080/SpringREST/rest/users/3", user);
    }
}

@RunWith(Suite.class)
@Suite.SuiteClasses({
    ListUsersTest.class,
    AddUserTest.class,
    UpdateUserTest.class,
    DeleteUserTest.class
})
public class UserRestControllerTestSuite {

}

其中最后一个测试类封装了前面四个测试类并按顺序运行他们。这样的顺序保证了运行测试之后不会对数据造成任何修改。