ramostear.comramostear.com 谭朝红的技术分享博客

格言 编程是一门技术,也是一门艺术 !

使用Spring Boot 2.0 十分钟构建Web应用程序

使用Spring Boot 2.0 十分钟构建Web应用程序

Spring Boot 2.0 :快速构建Web应用程序

1. 简介

Spring Boot 节约了我们对Spring Framework的学习和使用成本,它能够让我们以更短的时间,更高的效率去构建一个Web应用程序。本教程是Spring Boot 2.0系列教程的一个开端,我将在此文章中介绍一些Spring Boot 2.0的核心配置、前端应用、快速数据操作以及异常处理。

2. 配置

首先,我们使用Spring Initializr为我们的项目构建出基本的工程目录结构。

生成的项目将依赖于spring-boot-starter-parent:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.3.RELEASE</version>
    <relativePath/>
</parent>

一开始的项目依赖关系很简单,pom文件中添加的核心依赖如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

大多数的Spring库都可以通过简单的配置相关的starters导入到我们的项目中。

3. 应用程序配置

接下来,我们将为我们的应用程序配置一个简单的主类(主类的配置Spring Initializer已经为我们完成):

@SpringBootApplication
public class Application{
    public static void main(String[] args){
        SpringApplication.run(Application.class,args);
    }
}

说明:@SpringBootApplication 是一个组合注解,它等同于@Configuration@EnableAutoConfiguration@ComponentScan 三个注解的组合使用

最后,我们在定义一个简单的application.properties文件,设置一个简单的属性和值:

server.port = 8888

server.port = 8888 将把Spring Boot 设置的服务器端口从默认的8080更改为8888;其他属性的设置可参考Spring Boot 属性设置

4. MVC视图

在本次教程中,我们将使用Thymeleaf作为前端的模板,你也可以使用诸如Freemarker等其他的模板引擎。

首先,我们需要在pom文件中添加Thymeleaf的依赖,将spring-boot-starter-thymeleaf依赖项添加到pom.xml中:

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

通过上述配置,Thymeleaf已经引入到项目中,我们只需要在application.properties中简单的配置即可使用它:

spring.thymeleaf.cache = false
spring.thymeleaf.enabled = true
spring.thymeleaf.prefix = classpath:/templates/
spring.thymeleaf.suffix = .html
spring.application.name = Building Web Application base on Spring Boot 2.0

接下来,我们将定义一个简单的控制器和一个前端页面,主页控制器如下:

@Controller
public class HomeController{
    @Value("${spring.application.name}")
    String appName;

    @GetMapping("/")
    public String home(Model model){
        model.addAttribute("appName",appName);
        return "home";
    }
}

最后,创建一个home.html页面:

<html>
    <head>
        <title>Home Page</title>
    </head>
    <body>
        <h1>
            Spring Boot 2.0 Tutorial
        </h1>
        <p>
            Server message: <span th:text="${appName}">default appName</span>
        </p>
    </body>
</html>

5. 应用安全

接下来,我们将使用Spring Security为我们的应用程序增加安全访问控制,首先是添加相关的依赖项:

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

一旦我们导入并开启了Spirng Security,Spring Security将使用默认的httpBasic或者formLogin策略对我们的应用程序进行安全检查。也就是说默认情况下,我们应用中的所有端点都将被Spring Security进行安全保护,因此,我们需要通过扩展Spring Security的WebSecurityConfigurerAdapter类来自定义我们自己的自定义安全控制配置项:

@Configuration
@EnableWebSecurity
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter{
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests()
            .anyRequest()
            .permitAll()
            .and.csrf().disabled();
    }
}

通过上述的配置,在我们的应用中,所有端点的访问将不受任何限制。

Spring Security的内容不仅仅只是文中提到的这些,更多的内容可自行研究

6. 持久化

首先,让我们从定义数据模型开始,数据模型是持久化操作的基础。一个简单的用户实体:

@Entity
public class User{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(nullable = false,unique = true)
    private String username;

    @Column(nullable = false)
    private String address;
}

接下来,我们使用Spring Data来定义用户的存储库:

public interface UserRepository extends CrudRepository<User,Long>{
    List<User> findByUsername(String username);
}

最后,我们需要配置我们的持久化类:

@EnableJpaRepositories("com.ramostear.persistence.repo")
@EntityScan("com.ramostear.persistence.model")
@SpringBootApplication
public class Application{
    ...
}

相关注解说明:

为了快速的演示项目,我们使用了H2内存数据库,以减少项目在运行过程中所依赖的外部系统。一旦我们引入了H2的依赖项,Spring Boot会自动检测并设置我们的持久化关系,我们只需要在application.xml中配置数据源属性:

spring.datasource.driver-class-name = org.h2.Driver
spring.datasource.url = jdbc:h2:mem:bootapp;DB_CLOSE_DELAY=-1
spring.datasource.username = sa
spring.datasource.password =

7. Web层和控制器

在本小节中,我们将定义一个简单的控制器 —UserController来完成Web层的操作。我们将实现基本的CRUD操作,并通过REST 的方式来暴露User相关资源:

@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping
    public Iterable<User> findAll(){
        return userRepository.findAll();
    }

    @GetMapping("/username/{username}")
    public List<User> findByUsername(@PathVariable(name = "username")String username){
        return userRepository.findAllByUsername(username);
    }

    @GetMapping("/{id}")
    public User findOne(@PathVariable Long id){
        return userRepository.findById(id)
                .orElseThrow(UserNotFoundException::new);
    }


    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public User create(@RequestBody User user){
        User u = userRepository.save(user);
        return u;
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id){
        userRepository.findById(id)
                .orElseThrow(UserNotFoundException::new);
        userRepository.deleteById(id);
    }

    @PutMapping("/{id}")
    public User updateUser(@RequestBody User user,@PathVariable Long id){
        if(!id.equals(user.getId())){
            throw new UserIdMismatchException();
        }
        userRepository.findById(id)
                .orElseThrow(UserNotFoundException::new);
        return userRepository.save(user);
    }
}

鉴于文章篇幅问题,我们不再去定义前端页面,而是使用@RestController注解,将控制器定义为一个API控制器。这里的@RestController相当于@Controller@ResponseBody两个注解的组合。

8 . 异常处理

现在,Web应用的核心功能已经准备就绪,接下来我们将使用@ControllerAdvice注解来集中处理应用中可能出现的异常信息。

首先,我们需要定义一个自定义的异常类:

public class UserNotFoundException extends RuntimeException{
    public UserNotFoundException(String message,Throwable cause){
        super(message,cause);
    }
    ....
}

然后,使用@ControllerAdvice定义我们的异常信息统一处理类:

@ControllerAdvice
public RestExceptionHandler extends ResponseEntityExceptionHandler{

    @ExceptionHandler({UserNotFoundException.class})
    protected ResponseEntity<Object> handleNotFound(Exception ex,WebRequest request){
        return handleExceptionInternal(ex,"User not found",
                                       new HttpHeaders(),
                                       HttpStatus.NOT_FOUND,request);
    }

    @ExceptionHandler({UserIdMIsmatchException.class,
                      ConstraintViolationException.class,
                      DataIntegrityViolationException.class})
    public ResponseEntity<Object> handleBadRequest(Exception ex,WebRequest request){
        return handleExceptionInternal(ex,ex.getLocalizedMessage(),
                                      new HttpHeaders(),HttpStatus.BAD_REQUEST,request);
    }
}

默认情况下,Spring Boot 已提供了错误信息页面映射。我们也可以自定义一个error.html页面来定义其视图:

<html>
    <head>
        <title>Error Page</title>
    </head>
    <body>
        <h1>
            Error infomation !
        </h1>
        <b>
            [<span th:text="${status}">status</span>]
            <span th:text="${error}">error</span>
        </b>
        <p th:text="${message}">
            message
        </p>
    </body>
</html>

与设置Spring Boot中其他属性一样,我们可以在application.properties文件中覆盖默认的映射路径:

server.error.path = /error

9 . 应用测试

最后,我们将对我们定义的用户访问接口进行测试。我们将使用@SpringBootTest注解来加载应用程序的上下文:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class},webEnvironment = WebEnvironment.DEFINED_PORT)
public class UserAPITest{
    private static final String API_ROOT_URL = "http://localhost:8888/api/users";

     private User createRandomUser(){
        final User user = new User();
        user.setUsername(RandomStringUtils.randomAlphabetic(10));
        user.setAddress(RandomStringUtils.randomAlphabetic(15));
        return user;
    }

    private String  createUserAsUri(User user){
        final Response response = RestAssured.given()
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .body(user)
                .post(API_ROOT_URL);
        return API_ROOT_URL+"/"+response.jsonPath().get("id");
    }

}

首先,我们通过不同使用不同条件来查找用户:

@Test
    public void whenGetAllUsers_thenOk(){
        final Response response = RestAssured.get(API_ROOT_URL);
        Assert.assertEquals(HttpStatus.OK.value(),response.getStatusCode());
    }

    @Test
    public void whenGetUsersByUsername_thenOk(){
        final User user = createRandomUser();
        createUserAsUri(user);
        final Response response = RestAssured.get(API_ROOT_URL+"/username/"+user.getUsername());
        Assert.assertEquals(HttpStatus.OK.value(),response.getStatusCode());
       Assert.assertTrue(response.as(List.class).size() >0);
    }

    @Test
    public void whenGetCreatedUserById_thenOk(){
        final User user = createRandomUser();
        final String location = createUserAsUri(user);
        System.out.println(location);
        final Response response = RestAssured.get(location);
        Assert.assertEquals(HttpStatus.OK.value(),response.getStatusCode());
        Assert.assertEquals(user.getUsername(),response.jsonPath().get("username"));
    }

    @Test
    public void whenGetNotExistUserById_thenNotFound(){
        final Response response = RestAssured.get(API_ROOT_URL+"/"+RandomStringUtils.randomNumeric(4));
        Assert.assertEquals(HttpStatus.NOT_FOUND.value(),response.getStatusCode());
    }

接下来,我们尝试测试新增一个用户:

    @Test
     public void whenCreateNewUser_thenCreated(){
        final User user = createRandomUser();
        final Response response = RestAssured.given()
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .body(user)
                .post(API_ROOT_URL);
                Assert.assertEquals(HttpStatus.CREATED.value(),
                            response.getStatusCode());
    }

    @Test
    public void whenInvalidUserByUsernameNull_thenError(){
        final User user = createRandomUser();
        user.setUsername(null);
        final Response response = RestAssured.given()
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .body(user)
                .put(API_ROOT_URL);
                    Assert.assertEquals(HttpStatus.METHOD_NOT_ALLOWED.value(),
                                    response.getStatusCode());
    }

然后尝试更新一个用户的信息:

@Test
    public void whenUpdateCreatedUser_thenUpdated(){
        final User user = createRandomUser();
        final String location = createUserAsUri(user);
        user.setId(Long.parseLong(location.split("api/users/")[1]));
        user.setUsername("newUsername");
        Response response = RestAssured.given()
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .body(user)
                .put(location);
        Assert.assertEquals(HttpStatus.OK.value(),response.getStatusCode());
        response = RestAssured.get(location);
        Assert.assertEquals(HttpStatus.OK.value(),response.getStatusCode());
        Assert.assertEquals("newUsername",response.jsonPath().get("username"));
    }

最后,我们试着去移除一个用户:

@Test
public void whenDeleteCreatedUser_thenOk(){
    final User user = createRandomUser();
    final String location = createUserAsUri(user);
    Response response = RestAssured.delete(location);
    Assert.assertEquals(HttpStatus.OK.value(),response.getStatusCode());

    response = RestAssured.get(location);
    Assert.assertEquals(HttpStatus.NOT_FOUND.value(),response.getStatusCode());
}

测试结果:

10 . 总结

此教程是Spring Boot 2.0 系列教程的快速预览篇,通过一个简单的Demo快速的对Spring Boot有一个初步的认识。由于篇幅有限,不可能涵盖所有的技术细节,Spring Boot 2.0的核心技术教程将在其他教程中一一进行介绍。你可以通过Github网站获取本教程的全部源码:https://github.com/ramostear/spring-boot-tutorial-part1

(转载本站文章请注明作者和出处:谭朝红-ramostear.com,未经允许请勿做任何商业用途)

发表评论