第一个SpringBoot程序

第一个SpringBoot程序

例子来自慕课网廖师兄的免费课程

2小时学会SpringBoot

Spring Boot进阶之Web进阶

使用IDEA新建工程,选择Spring Initializr,勾选Web一路next就搭建了一个最简单的SpringBoot工程。如下:

package com.shy.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootApplication {

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

@SpringBootApplication整合了三个常用的注解,分别是:

  • @ComponentScan:会自动扫描指定包下的全部标有@Component的类,并注册成bean,当然包括@Component下的子注解@Service,@Repository,@Controller;
  • @SpringBootConfiguration:可以当成Spring的标准配置注解@Configuration来使用。而@Configuration表明这是一个JavaConfig配置类。通常配合@Bean注解,@Bean注解告诉Spring这个方法将返回一个对象,该对象将会注册为Spring应用上下文中的bean;
  • @EnableAutoConfiguration:能够自动配置spring的上下文,试图猜测和配置你想要的bean类,它可以自动加载散布在应用各处的@Configuration。

配置文件相关

SpringBoot的配置文件可以使用xml和yml格式,比如使用yml格式

# 自定义属性
cupSize: b
age: 18
# 可以在yml里通过${}来引用
content: "cupSize: ${cupSize}, age: ${age}"

# 指定端口为8080,(不配置默认8080)
server:
  port: 8080

可以使用注解@Value("${...}")获取配置文件中的值,@Value和@Autowired注解作用类似。

Spring提供了两种在运行时求值的方式:

  • 属性占位符:${...}
  • Spring表达式语言(SpEL):#{...}

如果cupSize和age都是属于同一类属性下的子属性,比如都属于girl。

那么可以写成下面的形式:

girl:
  cupSize: b
  age: 18

在java中注入时,也不用一个个属性注入,可以注入girl的全部属性。不过需要将girl的属性抽象成一个java类。

package com.shy.springboot.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;



/**
 * 读取配置文件的信息并自动封装成实体类
 * 注入SpringBoot配置文件中前缀是"girl"的全部属性
 */
@Component
@ConfigurationProperties(prefix = "girl")
public class GirlProperties {
    private String cupSize;
    private Integer age;

    public String getCupSize() {
        return cupSize;
    }

    public Integer getAge() {
        return age;
    }
}

属性配置方式

  • @Value,从配置文件中注入属性
  • @ConfigurationProperties(prefix = "...")读取配置文件中对应前缀中的属性,并映射成对象实体

环境配置:

可以建立多个application-xxx.yml文件,然后在application.yml文件中配置其中一个环境.

比如我有application-dev.yml文件表示开发环境下的配置文件,application-prod.yml文件表示生产环境下的配置文件。那么再按application.yml中配置如下

spring:
  profiles:
    active: prod

就表示使用application-dev.yml中的配置。

一些常用注解

  • Controller,作用于类上,表示MVC中的控制层,可以被@ComponentScan扫描到并注入。用于处理Http请求,返回字符串代表的模板,如xx.jsp, xx.ftl。
  • @RestController,是@ResponseBody和@Controller的整合,处理http请求,可以返回实体对象或字符串,以json格式表示。
  • @ResqustMapping,配置url映射。可以在类上使用(作为类中方法的前缀),可以在方法上使用。
package com.shy.springboot.controller;

import com.shy.springboot.config.GirlProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/girl")
public class Hello {
    /**
     * 表示value中的值都可作为url路径,如果不指定请求方法method,那么GET和POST方式都可以,但是一般不推荐 
     */
    @RequestMapping(value = {"/hello", "/hi"}, method = RequestMethod.GET)
    public String hello() {
        return "Hello";
    }
}

  • @PathVariable,获取url路径中的数据
  • @RequstParam,获取请求参数中的值
  • @GetMapping,组合注解,是@RequestMapping(value = "...", method = RequestMethod.GET)的缩写形式,当然也有PostMapping了。
package com.shy.springboot.controller;

import com.shy.springboot.config.GirlProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/girl")
public class Hello {
    // 可以响应 http://localhost:8080/girl/hello/xx
    @RequestMapping(value = {"/hello/{id}"}, method = RequestMethod.GET)
    public String hello(@PathVariable("id") Integer id) {
        return "My id is " + id;
    }
    // 可以响应 http://localhost:8080/girl/hello?id=xx
    // required = false表示这个参数可以为空,defaultValue表示当参数为空时的默认值,因此访问http://localhost:8080/girl/hello,将使用默认值1。
    @RequestMapping(value = {"/hello"}, method = RequestMethod.GET)
    public String hello2(@RequestParam(value = "id", required = false,defaultValue = "1") Integer id) {
        return "My id is " + id;
    }
}
 

数据库配置

本例子使用JPA和MySQL,所以在pom中加入如下依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

在application.yml中配置数据源和jpa相关。

spring:
  profiles:
    active: dev
  # 以下使用了jpa和mysql  
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/dbgirl
    username: root
    password: admin
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true

JPA(Java Persistence API),即Java持久化API,Hibernate实现了这个规范。

package com.shy.springboot.database;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Girl {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private Integer age;
    private String cupSize;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getCupSize() {
        return cupSize;
    }

    public void setCupSize(String cupSize) {
        this.cupSize = cupSize;
    }

    public Girl() {
    }
}

  • @Entity 表示这是个实体类,可以映射成数据表。
  • @Id表示该属性为主键
  • @GeneratedValue表示该属性字段为自增,一般搭配@Id使用

@GeneratedValue有几种策略

  • IDENTITY:采用数据库ID自增长的方式来自增主键字段,Oracle 不支持这种方式,使用MySQL时,配置该策略可以实现主键自增。
  • AUTO:JPA自动选择合适的策略,是默认选项;
  • SEQUENCE:通过序列产生主键,通过@SequenceGenerator 注解指定序列名,MySql不支持这种方式 ;
  • TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植;

jpa.hibernate.ddl-auto,共有五种配置方式。

  • ddl-auto:create: 每次运行该程序,没有表格会新建表格,表内有数据会清空
  • ddl-auto:create-drop: 每次程序结束的时候会清空表
  • ddl-auto:update: 每次运行程序,没有表格会新建表格,若已经存在表格,则只会更新
  • ddl-auto:validate: 运行程序会校验数据与数据库的字段类型是否相同,不同会报错
  • none: 禁止DDL处理

Controller和几个简单的请求

Repository提供了最基本的数据访问功能,通过新建一个接口继承JpaRepository<T, ID>,可以直接使用接口中现成的方法来实现对数据的访问。

package com.shy.springboot.database;

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

import java.util.List;

public interface GirlRepo extends JpaRepository<Girl, Integer> {
    // 自定义的查询方法,方法名要严格按照一定规则来命名
    List<Girl> findByAge(Integer age);
}

泛型中的Girl表示该Repository可以访问由Girl映射的数据表,Integer表示ID的数据类型。

写一个Controller,处理各种请求来看JPA是如何与数据库交互的。

package com.shy.springboot.controller;

import com.shy.springboot.database.Girl;
import com.shy.springboot.database.GirlRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public class GirlController {

    @Autowired
    private GirlRepo girlRepo;

    /**
     * 查询所有女生
     * @return
     */
    @GetMapping("/girls")
    public List<Girl> girls() {
        return girlRepo.findAll();
    }

    /**
     * 添加一个女生
     * @param cupSize
     * @param age
     * @return
     */
    @PostMapping("/addGirl")
    public Girl addGirl(@RequestParam("cupSize") String cupSize,
                        @RequestParam("age") Integer age) {

        Girl girl = new Girl();
        girl.setAge(age);
        girl.setCupSize(cupSize);
        return girlRepo.save(girl);
    }

    /**
     * 通过id更新一个女生
     * @param id
     * @param cupSize
     * @param age
     * @return
     */
    @PostMapping("/updateGirl/{id}")
    public Girl updateGirl(@PathVariable("id") Integer id,
                           @RequestParam("cupSize") String cupSize,
                           @RequestParam("age") Integer age) {

        Girl girl = new Girl();
        girl.setId(id);
        girl.setAge(age);
        girl.setCupSize(cupSize);
        return girlRepo.save(girl);
    }

    /**
     * 根据id删除一个女生
     * @param id
     */
    @GetMapping("/deleteGirl/{id}")
    public void deleteGirl(@PathVariable("id") Integer id) {
        Girl girl = new Girl();
        girl.setId(id);
        girlRepo.delete(girl);
    }

    /**
     * 根据id查询一个女生
     * @param id
     * @return
     */
    @GetMapping("girls/{id}")
    public Girl girlFindOne(@PathVariable("id") Integer id) {
        return girlRepo.findById(id).get();
    }

    /**
     * 根据年龄查询一个女生
     * @param age
     * @return
     */
    @GetMapping("girls/age/{age}")
    public List<Girl> findGirlsByAge(@PathVariable("age") Integer age) {
        return girlRepo.findByAge(age);
    }
}

没有写一句SQL语句,就完成了对girl表的增删改查,用起来还是很舒服的。

事务管理

下面的insertTwo方法插入两条数据,如果不进行事务管理,则插入girlA成功,插入girlB失败。加上@Transactional注解后(有两个同名注解,导入spring的),要么两条数据都插入成功,要么两条都插入失败。因为在本例中会出现异常,所以两条都插入失败。

// Service中
package com.shy.springboot.service;

import com.shy.springboot.database.Girl;
import com.shy.springboot.database.GirlRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class GirlService {
    @Autowired
    private GirlRepo girlRepo;
    @Transactional
    public void insertTwo() {
        Girl girlA = new Girl();
        girlA.setCupSize("B");
        girlA.setAge(18);
        girlRepo.save(girlA);
        // 除0异常,退出
        int a = 3 / 0;
        Girl girlB = new Girl();
        girlB.setCupSize("C");
        girlB.setAge(20);
        girlRepo.save(girlB);
    }
}

// Controller中
@PostMapping("/girls/insertTwo")
    public void insertTwo() {
        girlService.insertTwo();
}

因为Hibernate创建的表默认引擎是MyISAM,所以如果发现事务没有作用,要手动修改引擎为InnoDB。

ALTER TABLE xxx ENGINE=INNODB;

表单验证

在上面的例子中如果要对年龄作限制,比如小于18岁的girl不能添加。可以在实体类中对其中的字段属性使用注解来加以限制。

@Min(value = 18, message = "未满18岁不得入内!")
private Integer age;

这句代码限制了girl的年龄不能低于18岁。在Controller中修改添加女生的逻辑

/**
 * 添加一个女生
 * @return
 */
@PostMapping("/addGirl")
public Girl addGirl(@Valid Girl girl, BindingResult result) {
    if (result.hasErrors()) {
        System.out.println(result.getFieldError().getDefaultMessage());
        return null;
    }
    return girlRepo.save(girl);
}

@Valid可以对对象进行验证,加了@Valid注解的参数,其后要紧跟着BindingResult或者Errors(前者是后者的实现类),用于保存验证结果。如果对象中有属性不满足验证条件,其结果将体现中BindingResult中。

AOP

首先在pom中添加依赖

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

然后编写切面

package com.shy.springboot.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class HttpAspect {
    private static final Logger LOG = LoggerFactory.getLogger(HttpAspect.class);

    @Pointcut("execution(public * com.shy.springboot.controller.GirlController.*(..))")
    public void log() {}

    @Before("log()")
    public void doBefore() {
        LOG.info("我在方法调用前执行");
    }

    @After("log()")
    public void doAfter() {
        LOG.info("我在方法调用后执行");
    }

}

因为在方法调用的前后都要对相同的方法进行通知,为了避免代码冗余,把@Before和@After的execution表达式抽取成切点。

@Pointcut("execution(public * com.shy.springboot.controller.GirlController.*(..))")

表示对GirlController中所有public的任意返回值、任意参数的方法进行通知。注意该注解需要用在方法上,所以public log() {}在这里只是起一个标识作用,供@Pointcut依附,所以它的方法体是空的。

该切面使用了slf4j的日志。当请求http://localhost:8080/addGirl时,控制台输出以下日志,可以显示比System.out.println()更详细的信息。

2018-10-03 10:03:46.899  INFO 1892 --- [nio-8080-exec-3] com.shy.springboot.aspect.HttpAspect     : 我在方法调用前执行
Hibernate: insert into girl (age, cup_size) values (?, ?)
2018-10-03 10:03:47.031  INFO 1892 --- [nio-8080-exec-3] com.shy.springboot.aspect.HttpAspect     : 我在方法调用后执行

输出的Hibernate: insert into girl (age, cup_size) values (?, ?)表示了Controller中addGirl方法的执行,在其前后分别输出了@Before和@After执行的逻辑,所以AOP确实是生效了的。

现在修改doBefore方法,使它能从Request域中获取请求url、IP地址、请求方法、请求中传递的参数。

@Aspect
@Component
public class HttpAspect {
    private static final Logger LOG = LoggerFactory.getLogger(HttpAspect.class);

    @Pointcut("execution(public * com.shy.springboot.controller.GirlController.*(..))")
    public void log() {}

    @Before("log()")
    public void doBefore(JoinPoint joinPoint) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // url
        LOG.info("url={}", request.getRequestURI());
        // ip
        LOG.info("IP={}", request.getRemoteAddr());
        // method
        LOG.info("method={}", request.getMethod());
        // 参数
        LOG.info("args={}", joinPoint.getArgs());
        // class-method
        LOG.info("class_method={}", joinPoint.getSignature().getDeclaringTypeName() + " " + joinPoint.getSignature().getName());

        LOG.info("我在方法调用前执行");
    }

    @AfterReturning(value = "log()",returning = "obj")
    public void doAfterReturning(Object obj) {
        if (obj != null) {
            LOG.info("Girl={}", obj.toString());
        }
    }

    @After("log()")
    public void doAfter() {
        LOG.info("我在方法调用后执行");
    }

}

在通知方法中可以声明一个JoinPoint类型的参数,通过JoinPoint可以访问连接点的细节。

  • getArgs():获取连接点方法运行时的入参列表;
  • getSignature() :获取连接点的方法签名对象;
  • getSignature().getName():获取连接点的方法名
  • getSignature().getDeclaringTypeName():获取连接点所在类的名称

还新增了一个@AfterReturning的通知,在方法成功返回后执行(若抛出异常将不会执行该通知),和@After的区别在于:被增强的方法不论是执行成功还是抛出异常,@After通知方法都会得到执行。

AOP中 @Before @After @AfterThrowing @AfterReturning的执行顺序如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   Object result;
   try {
       // @Before
       result = method.invoke(target, args);
       // @After
       return result;
   } catch (InvocationTargetException e) {
       Throwable targetException = e.getTargetException();
       // @AfterThrowing
       throw targetException;
   } finally {
       // @AfterReturning
   }
}

可知@AfterReturning的执行在@After之后。

如果请求http://localhost:8080/addGirl,将输出以下日志(日志一些无关紧要的内容已被删除)

url=/addGirl
IP=0:0:0:0:0:0:0:1
method=POST
args=Girl{id=null, age=26, cupSize='C'}
class_method=com.shy.springboot.controller.GirlController addGirl
我在方法调用前执行
Hibernate: insert into girl (age, cup_size) values (?, ?)
我在方法调用后执行
Girl=Girl{id=27, age=26, cupSize='C'}

统一异常处理

前面的addGirl方法,当验证不通过时,返回null并在控制台打印相关信息;当验证通过又返回Girl。返回值不统一,而且如果我们希望将错误信息显示在页面,怎么办呢?

可定义一个Result<T>,将要呈现的信息统一化,分别是错误码code,错误信息msg和承载的对象T,这样不管是成功还是发生各种各样的异常,都可以返回统一的Result对象。

package com.shy.springboot.domain;

public class Result<T> {
    /** 错误码 */
    private Integer code;
    /** 信息 */
    private String msg;
    /** 对象 */
    private T data;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

再写一个工具类,可在成功和异常时候设置对应的状态和信息,可有效减少重复代码。

package com.shy.springboot.util;

import com.shy.springboot.domain.Result;

public class ResultUtil {
    public static Result success(Object obj) {
        Result result = new Result();
        result.setMsg("成功");
        result.setCode(0);
        result.setData(obj);
        return result;
    }

    public static Result success() {
        return success(null);
    }

    public static Result error(Integer code, String msg) {
        Result result = new Result();
        result.setMsg(msg);
        result.setCode(code);
        return result;
    }
}

于是我们的addGirl方法可以重构成下面的样子

@PostMapping("/addGirl")
public Result<Girl> addGirl(@Valid Girl girl, BindingResult bindingResul) {
    if (bindingResul.hasErrors()) {
        return ResultUtil.error(1,bindingResul.getFieldError().getDefaultMessage());
    }
    girlRepo.save(girl);
    return ResultUtil.success(girl);
}

现在新增一个检查年龄的逻辑,小于14岁的认为在上小学,14~17岁认为在上初中,这两种情况都不允许其进入,当检查到年龄不符合要求时,抛出异常。

在GirlService中

public void checkAge(Integer id) {
    Girl girl = girlRepo.findById(id).get();
    int age = girl.getAge();
    if (age < 14) {
        throw new GirlException(ResultEnum.PRIMARY_SCHOOL);
    } else if (age < 17) {
        throw new GirlException(ResultEnum.MIDDLE_SCHOOL);
    }
    // 其他年龄的逻辑处理
}

注意上面使用枚举来统一管理各种code对应的msg。

package com.shy.springboot.enums;

public enum ResultEnum {
    SUCCESS(0, "成功"),
    ERROR(-1, "未知错误"),
    PRIMARY_SCHOOL(100, "你可能还在上小学"),
    MIDDLE_SCHOOL(101, "你可能还在上初中");

    private Integer code;
    private String msg;

    ResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

GirlException是个自定义异常类,除了message还把code整合进去了。

package com.shy.springboot.exception;

import com.shy.springboot.enums.ResultEnum;

public class GirlException extends RuntimeException{
    private Integer code;

    public GirlException(ResultEnum resultEnum) {
        super(resultEnum.getMsg());
        this.code = resultEnum.getCode();
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }
}

在Controller中只是简单调用下Service中的方法而已

@GetMapping("/girlAge/{id}")
public void getAge(@PathVariable("id") Integer id) {
    girlService.checkAge(id);
}

如果现在启动程序,请求http://localhost:8080/girlAge/22, 将按照自定义异常,但是返回的结果其格式是下面这样的:

{
    timestamp: 14XXXXXXX,
    status: 500,
    exception: XXX,
    message: XXX,
    path: "/girlAge/22"
}

因为系统内部发生了错误,不断往上抛异常就会得到上面的信息。如果要保持不管在什么情况下统一返回Result<T>中的信息,像下面这样:

{
    code: xxx,
    msg: xxx,
    data: XXX
}

则需要对异常做一个捕获,取出有用的message部分,然后再封装成Result对象,再返回给浏览器。为此新建一个异常捕获类

package com.shy.springboot.handle;

import com.shy.springboot.domain.Result;
import com.shy.springboot.exception.GirlException;
import com.shy.springboot.util.ResultUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * ControllerAdvice注解将作用在所有注解了@RequestMapping的控制器的方法上。
 * 配合@ExceptionHandler,用于全局处理控制器里的异常
 */
@ControllerAdvice
public class ExceptionHandle {
    private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandle.class);

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result handle(Exception e) {
        if (e instanceof GirlException) {
            GirlException exception = (GirlException) e;
            return ResultUtil.error(exception.getCode(),exception.getMessage());
        }
        LOG.error("系统异常:{}", e.getMessage());
        return ResultUtil.error(-1,"未知错误");
    }
}

该类使用了注解@ControllerAdvice,@ControllerAdvice会作用在所有注解了@RequestMapping的控制器的方法上,再配合@ExceptionHandler,用于全局处理控制器里的异常。@ExceptionHandler(Exception.class)表示可以处理Exception类及其子类。

因为除了会抛出自定义异常GirlException外,还有可能因为系统原因抛出其他类型的异常(如空指针异常),因此针对不同类型的异常返回不同的状态码,上面使用了instanceof来判断异常类型。如果不是GirlException,被统一归类为未知错误,但是各种异常都显示未知错误不便于排查问题,因此在可控制台输出了异常原因来加以区分。

单元测试

SpringBoot中进行单元测试十分便捷,SpringBoot中默认使用了Junit4。

在src/test下可以创建单元测试类,当然更简单的方法是在IDEA下右键,Go To -> Test Subject,然后选择想要进行测试的方法即可。

下面的单元测试针对service层,主要是判断某数据库中某id的girl,其年龄实际值和预期值是否一致。有两个比较关键的注解

  • @RunWith(SpringRunner.class):当一个类用@RunWith注释或继承一个用@RunWith注释的类时,JUnit将调用它所引用的类来运行该类中的测试而不是开发者去在Junit内部去构建它,因此这句代码意思是让测试运行于Spring测试环境中,SpringRunner仅仅继承了SpringJUnit4ClassRunner而已,并没有扩展什么功能,前者可以看作是后者的“别名”。
  • @SpringBootTest:可以自动搜寻@SpringBootConfiguration;在没有明确指定@ContextConfiguration(loader=...)时,使用SpringBootContextLoader作为默认的ContextLoader,等等。
package com.shy.springboot.service;

import com.shy.springboot.domain.Girl;
import com.shy.springboot.repository.GirlRepo;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * RunWith(SpringRunner.class),让测试运行于Spring测试环境
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class GirlServiceTest {
    @Autowired
    private GirlRepo girlRepo;
    @Test
    public void findOne() {
        Girl girl = girlRepo.findById(22).get();
        Assert.assertEquals(new Integer(14), girl.getAge());
    }
}

然后针对Controller层,对某次请求进行测试,这里使用到了MockMvc。

package com.shy.springboot.controller;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class GirlControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    public void girls() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/girls")).andExpect(MockMvcResultMatchers.status().isOk());
        /* 下面这条测试不能通过 */
        // mockMvc.perform(MockMvcRequestBuilders.get("/girls")).andExpect(MockMvcResultMatchers.content().string("abc"));
    }
}

第一条测试模拟以get方法请求/girls,并期望状态码是200 OK。注释掉的第二条测试期望响应的内容是abc,然而我们返回的是json格式,所以肯定不能通过测试的。


2018.10.4

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容