-- 1 -- springboot

框架或工具:Lombok
项目地址:https://github.com/h837272998/next-springboot

一、Maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hjh.myspringboot</groupId>
    <artifactId>myspringboot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>myspringboot</name>
    <description>自学习springboot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

二、配置文件

springboot 提供了两种配置文件格式。分别是properties和yml。选择一种使用,结果都一样。这里选择properties。(为了看懂其他项目两者最好都掌握,喜欢用哪个看个人习惯)

server.port t端口
server.servlet.context-path 上下文路径
spring.datasource.* 数据库连接配置

server.port=8080
server.servlet.context-path=

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring?serverTimezone=GMT&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456

三、RESTful API

常见的API

操作 url method
增加 /user/add?name=xx POST
删除 /user/delete?id=x GET
修改 /user/update?id=x&name=xxx POST
获取 /user/get?id=x GET
查询 /user/list?name=xx GET

采用RESTful API
| 操作 | url | method |
| --- | --- | --- |
| 增加 | /user | POST |
| 删除 | /user/x | DELETE |
| 修改 | /user/x | PUT |
| 获取 | /user/x | GET |
| 查询 | /user?name=xx | GET |

RESTful架构:
遵循统一接口原则,统一接口包含了一组受限的预定义操作,不论什么资源,都是通过使用相同的接口进行资源访问
RESTful 是一种风格,不是强制标准。

四、编写RESTful和测试用例。

实现对用户表的增删改查

User.java

@Data
public class User {
    @JsonView(View.Summary.class)
    private long id;

    @JsonView(View.Summary.class)
    private String username;

    @JsonView(View.SummaryWithDetail.class)
    private String password;

    @JsonView(View.SummaryWithDetail.class)
    private Date createDate;
}

View.java

public class View {
    public interface Summary{}
    public interface SummaryWithDetail extends Summary{}
}

UserController.java

/**
 * @Description:
 * @Author: HJH
 * @Date: 2019-08-16 21:32
 */
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping
    public User add(@RequestBody User user){
        log.info("增加用户;"+user.toString());
        user.setId(1);
        return user;
    }


    //正则匹配 id只能为数字
    @DeleteMapping("/{id:\\d+}")
    public void delete(@PathVariable long id){
        log.info("删除用户id:"+id);
    }

    @PutMapping("/{id:\\d+}")
    public User update(@RequestBody User user){
        log.info("修改用户:"+user);
        return user;
    }

    @GetMapping("/{id:\\d+}")
    public User get(@PathVariable long id){
        User user = new User();
        user.setId(1);
        user.setUsername("hjh");
        user.setPassword("123");
        return user;
    }

    @GetMapping
    @JsonView(View.Summary.class)
    public List<User> list(User user){
        log.info("查询用户名:" + user);
        ArrayList<User> users = new ArrayList<>();
        users.add(new User());
        users.add(new User());
        users.add(new User());
        return users;
    }

}

测试用例
UserControllerTest.java

// 引入静态对象。简化
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * @Description:
 * @Author: HJH
 * @Date: 2019-08-16 21:38
 */
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void before(){
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void whenAddSuccess() throws Exception {
        String content = "{\"username\":\"hjh\",\"password\":null,\"createDate\":"+new Date().getTime()+"}";
        String result = mockMvc.perform(post("/user")
        .contentType(MediaType.APPLICATION_JSON_UTF8)
        .content(content))  //请求
                .andExpect(status().isOk()) //断言响应结果
                .andExpect(jsonPath("$.id").value(1))
                .andReturn().getResponse().getContentAsString();
        log.info("增加结果;"+result);
    }

    @Test
    public void whenUpdateSuccess() throws Exception {
        String content = "{\"username\":\"hjh\",\"password\":null,\"createDate\":"+new Date().getTime()+"}";
        String result = mockMvc.perform(put("/user/1")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(content))  //请求
                .andExpect(status().isOk()) //断言响应结果
                .andReturn().getResponse().getContentAsString();
        log.info("修改结果;"+result);
    }

    @Test
    public void whenDeleteSuccess() throws Exception {
        String result = mockMvc.perform(delete("/user/1")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isOk()) //断言响应结果
                .andReturn().getResponse().getContentAsString();
    }

    @Test
    public void whenGetSuccess() throws Exception {
        String result = mockMvc.perform(get("/user/1")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isOk()) //响应结果
                .andReturn().getResponse().getContentAsString();
        log.info("获得结果;"+result);
    }

    @Test
    public void whenListSuccess() throws Exception {
        String result = mockMvc.perform(get("/user").param("username","hjh")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isOk()) //断言响应结果
                .andReturn().getResponse().getContentAsString();
        log.info("查询结果;"+result);
    }
}

Jackson @JsonVIew
@JsonView可以过滤序列化对象的字段属性,使有选择的序列化对象
可以将View类理解为一组标识,Summary只是其中一种标识,其中DetailSummary继承了Summary
当使用@JsonView序列化User对象的时候,就只会序列化选择的属性,可以隐藏一些不想序列化的字段属性。
简单的说就是可以控制控制器层某个方法输出对象的属性。例如在查看用户详细信息的时候可以看到用户的所有信息。当查询所有用户时就不显示密码。

五、数据验证

数据验证:对前台传输的数据进行验证。

  1. 在entity定义对应成员变量的验证
    @Past:过去的时间
    @NotBlank 不能为空

  1. 需要验证的方法中添加@Valid注解
    再通过BindingResult捕获错误

1. 常见的验证

2. 自定义消息

通过重写message

3. 自定义校检注解

定义注解MyConstraint
在注解里面的注解称为元注解,例如Target...
Target 作用目标:作用在METHOD(方法)和FIELD(字段)
Retention 保留位置:RetentionPolicy.RUNTIME : 注解保留在程序运行期间,此时可以通过反射获得定义在某个类上的所有注解。

MyConstraint.java

@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyConstraintValidate.class)
public @interface MyConstraint {

    String message() default "测试验证";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}

注解实现类

MyConstraintValidate.java

@Slf4j
public class MyConstraintValidate implements ConstraintValidator<MyConstraint, Object> {
    @Override
    public void initialize(MyConstraint constraintAnnotation) {
        //初始化
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        log.info("MyConstraintValidate: ",value);
        return false;
    }
}

通过isValid 判断值是否通过验证

六、异常处理

1. springboot原生异常

覆盖默认的处理方式

  1. 自定义一个bean,实现ErrorController接口,那么默认的错误处理机制将不再生效。
  2. 自定义一个bean,继承BasicErrorController类,使用一部分现成的功能,自己也可以添加新的public方法,使用@RequestMapping及其produces属性指定新的地址映射。
  3. 自定义一个ErrorAttribute类型的bean,那么还是默认的两种响应方式,只不过改变了内容项而已。
  4. 继承AbstractErrorController

BasicErrorController.class

SpringBoot在页面 发生异常的时候会自动把请求转到/error,SpringBoot内置了一个BasicErrorController对异常进行统一的处理.

浏览器404

应用程序404(postMan)

分析

修改浏览器响应的404
在templates添加error/404
404.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>404</title>
</head>
<body>

    <h1>访问的页面不存在</h1>
    <p th:text="'状态码:'+${status}"></p>
    <p th:text="'错误:'+${error}"></p>
    <p th:text="'错误信息:'+${message}"></p>
    <p th:text="'路径:'+${path}"></p>
    <p th:text="'时间戳:'+${timestamp}"></p>
</body>
</html>

结果:

error/.. 如果需要模板渲染需要放在渲染路径。像thymeleaf,如果放在 classpath:static/errror/. 就不会被渲染,从而无法获得status等数据。

2. 自定义异常类和全局异常

  1. 自定义异常
    UserNotExistException.java
public class UserNotExistException extends RuntimeException {
    private long id;

    public UserNotExistException(long id){
        super("the user is not exist...");
        this.id = id;
    }
    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
}

进行测试

@GetMapping("/exceptionTest")
    public void test(){
        throw new UserNotExistException(1);
    }

结果测试:

添加Controller全局异常捕获

ControllrExceptionhandler.java

/**
 * @Description:全局控制器异常处理器
 * @Author: HJH
 * @Date: 2019-08-17 15:43
 */
@ControllerAdvice
public class ControllerExceptionHandler {


    /**
     * @Description:处理UserNotExistException,返回的是json对象
     * @Author: HJH
     * @Date: 2019-08-17 16:46
     * @Param: [ex]
     * @Return: java.util.Map<java.lang.String,java.lang.Object>
     */
    @ExceptionHandler(UserNotExistException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String,Object> handleUserNotExistException(UserNotExistException ex){
        Map<String,Object> result = new HashMap<>();
        result.put("id",ex.getId());
        result.put("message", ex.getMessage());
        return result;
    }

    /**
     * @Description:全局Exception处理。返回的是html页面
     * @Author: HJH
     * @Date: 2019-08-17 15:56
     * @Param: [req, e]
     * @Return: ModelAndView
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception ex) throws Exception {
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", ex);
        mav.addObject("url", req.getRequestURL());
        mav.setViewName("error/errorPage");
        return mav;
    }

}

测试控制器

@GetMapping("/exceptionTest")
    public void test(@RequestParam String ex){
        if("runtime".equals(ex)){
            throw new RuntimeException("this is a RuntimeException");
        }
        throw new UserNotExistException(1);
    }

测试结果

  1. /user/exceptionTest?ex=user
    使用浏览器和app(PostMan)

  2. /user/exceptionTest?ex=runtime
    使用浏览器和app(PostMan)

通过上面就可以对控制器层的异常进行捕获
弊端,只能返回页面或者json格式。在需要json格式异常时也只是返回页面

改进

@ExceptionHandler(value = Exception.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Object defaultErrorHandler(HttpServletRequest req, Exception ex) throws Exception {

        if ("application/json".equals(req.getHeader("Content-Type"))){
            Map<String, Object> result = new HashMap<>();
            result.put("message", ex.getMessage());
            return result;
        }else {
            ModelAndView mav = new ModelAndView();
            mav.addObject("exception", ex);
            mav.addObject("url", req.getRequestURL());
            mav.setViewName("error/errorPage");
            return mav;
        }
    }

测试结果:还是ok的。但没有真正的测试 ,当是ajax请求抛出错误是否能够判断。但思路应该是可行的
虽然加了@ResponseBody但是返回是ModelAndView时还是可以解析成页面

七、对API的拦截

1. 过滤器(Filter)

定义一个时间过滤器,实现对API访问进行计时

@Slf4j
@Component
public class TimeFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("TimeFilter Init");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("time filter start");
        long begin = System.currentTimeMillis();
        filterChain.doFilter(servletRequest,servletResponse);
        log.info("the request spent :"+(System.currentTimeMillis()-begin));
        log.info("time filter finish");
    }

    @Override
    public void destroy() {
        log.info("TimeFilter Destroy");
    }
}

使用@Component 就可以使过滤器生效

调用一个API,结果

自定义的拦截器可以利用注解实现注入使过滤器生效。但是其他第三方过滤器时该如何配置。
添加配置文件。将timeFilter的@Component去掉使用下面也可以实现添加过滤器。

WebConfig.java

@Configuration
public class WebConfig {
    @Bean
    public FilterRegistrationBean timeFilter(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        TimeFilter timeFilter = new TimeFilter();
        registrationBean.setFilter(timeFilter);

        List<String> urls = new ArrayList<>();
        urls.add("/*");  //拦截url
        registrationBean.setUrlPatterns(urls);
        return registrationBean;
    }
}

2. 拦截器(Interceptor)

在过滤器中。使用的Filter

是在包 javax.servlet中定义的。并不知道spring的那一套

使用拦截器实现运行时间。但可以获取执行函数
TimeInterceptor.java

/**
 * @Description:
 * @Author: HJH
 * @Date: 2019-08-17 19:31
 */
@Slf4j
@Component
public class TimeInterceptor implements HandlerInterceptor {

    /**
     * @Description:控制器方法调用之前
     * @Author: HJH
     * @Date: 2019-08-17 19:35
     * @Param: [request, response, handler]
     * @Return: boolean
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        log.info("Interceptor perHandle");
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        log.info("Interceptor Class Name: "+handlerMethod.getBean().getClass().getName());
        log.info("Interceptor Method Name: "+handlerMethod.getMethod().getName());
        request.setAttribute("startTime",System.currentTimeMillis());
        return true;
    }
    /**
     * @Description:控制器完成之后。但当控制器抛出错误时就不会进入该函数
     * @Author: HJH
     * @Date: 2019-08-17 19:36
     * @Param: [request, response, handler, modelAndView]
     * @Return: void
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           @Nullable ModelAndView modelAndView) throws Exception {
        log.info("Interceptor postHandle");
        long start = (long) request.getAttribute("startTime");
        log.info("Time Interceptor Spent:"+(System.currentTimeMillis()-start));
    }

    /**
     * @Description:类似try-catch 的finally
     * @Author: HJH
     * @Date: 2019-08-17 19:37
     * @Param: [request, response, handler, ex]
     * @Return: void
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                @Nullable Exception ex) throws Exception {
        log.info("Interceptor afterCompletion");
        long start = (long) request.getAttribute("startTime");
        log.info("Time Interceptor Spent:"+(System.currentTimeMillis()-start));
        log.info("Exception is "+ex);
    }
}

WebInterceptorConfig.java

@Configuration
public class WebInterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private TimeInterceptor timeInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(timeInterceptor);
    }
}

运行结果

可以看到 afterCompletion一定会执行的

3. 切片(Aspect)

spring aop

切片实现
要点:切入点 (通过注解)1. 在什么方法上起作用。2. 在什么时候起作用。
要点:增强(通过方法):起作用时执行的业务逻辑

TimeAspect.java

@Slf4j
@Aspect
@Component
public class TimeAspect {

    @Around("execution(* com.hjh.myspringboot.myspringboot.web.controller.UserController.*(..))")
    public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {

        log.info("Time Aspect Start");
        //获取传输args
        Object[] args = pjp.getArgs();
        for (Object arg:args){
            log.info("arg is:" +arg);
        }
        long start = System.currentTimeMillis();
        Object o = pjp.proceed();
        log.info("Time Aspect spent:"+(new Date().getTime()-start));
        log.info("Time Aspect end");
        return o;
    }
}

结果

总结

拦截顺序

七、文件上传下载

比较简单的实现和单元测试
增加单元测试

@Test
    public void whenUploadSuccess() throws Exception {
        String result = mockMvc.perform(multipart("/file")
        .file(new MockMultipartFile("file","test.txt","multipart/form-data","hello".getBytes("utf-8"))))
                .andExpect(status().isOk())
        .andReturn().getResponse().getContentAsString();
        log.info("上传结果"+result);
    }

fileupload在springboot2.0x过时。

FileController

@Slf4j
@RestController
@RequestMapping("/file")
public class FileController  {

    public static final String  FOLDER = "D:/U";

    @PostMapping
    public Map upload(MultipartFile file) throws IOException {
        log.info("上传文件名"+file.getName());
        log.info("上传文件原始名"+file.getOriginalFilename());
        log.info("上传文件大小:"+file.getSize());


        File file1 = new File(FOLDER,System.currentTimeMillis()+".txt");
        file.transferTo(file1);
        Map<String, String> map = new HashMap<>();
        map.put("path",file1.getAbsolutePath());
        return map;
    }

    @GetMapping("/{id}")
    public void download(@PathVariable String id, HttpServletResponse response, HttpServletRequest request) throws IOException {

        try(FileInputStream inputStream = new FileInputStream(new File(FOLDER, id + ".txt"));
            OutputStream outputStream = response.getOutputStream()) {
                response.setContentType("application/x-download");
                response.addHeader("Content-Disposition","attachment;filename=test.txt");

            IOUtils.copy(inputStream,outputStream);
            outputStream.flush();
        }
    }

}

八、异步处理

当同时访问一个需要处理10s的方法时

结果

通过输出:
第一个访问过来10s后第二个访问才被处理。也就是说在当前方法中。同一时间只能处理一个请求。只有当前请求处理完后才能处理下一个。

1、使用Runable异步

@GetMapping("async2")
    public Callable<String> async2(){
        log.info("主线程开始");
        Callable<String> result = new Callable<String>() {
            @Override
            public String call() throws Exception {
                log.info("副线程开始");
                for (int i=0;i<10;i++){
                    Thread.sleep(1000);
                }
                log.info("副线程返回");
                return "success";
            }
        };
        log.info("主线程结束");
        return result;
    }

在同一时间可以处理同一类请求,提高了服务器的吞吐量
场景比较单一,添加一个副线程。

2. 使用DeferredResult异步处理请求

使用消息队列解决请求丢失等问题,可以用于订单处理等,重要的,高可用,高并发的请求。

下面使用异步模拟处理订单

使用RibbitMq作为消息队列

添加maven依赖

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

添加mq配置

spring.application.name=spring-boot
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456

添加springboot配置
使用Exchange类型的Direct。添加两条队列。一个用于接收下单,一个接收订单处理结果。
RabbitMQConfig.java

Configuration
public class RabbitMQConfig {

    @Bean
    public Queue queue1(){
        return new Queue("order");
    }

    @Bean
    public Queue queue2(){
        return new Queue("finish");
    }
}

DeferredResultHolder.java

@Component
public class DeferredResultHolder {
    private Map<String , DeferredResult<String>> map = new HashMap<>();

    public Map<String, DeferredResult<String>> getMap() {
        return map;
    }

    public void setMap(Map<String, DeferredResult<String>> map) {
        this.map = map;
    }
}
  1. 线程1:
@Autowired
    private AmqpTemplate rabbitTemplate;

    @GetMapping("async4")
    public DeferredResult<String> async4() throws InterruptedException {
        log.info("主线程开始");
        String orderId = RandomStringUtils.randomNumeric(8);
        log.info("2.发送下单请求:"+orderId);
        rabbitTemplate.convertAndSend("order",orderId);
        DeferredResult<String> result = new DeferredResult();
        deferredResultHolder.getMap().put(orderId,result);
        return result;
    }
  1. 应用2:监听请求并处理
@Slf4j
@Component
@RabbitListener(queues = "order")
public class OrderReceiver {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RabbitHandler
    public void process(String re) {

        new Thread(()->{
            log.info("3.监听到下单请求:"+re);
            //处理
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //处理完成
            log.info("4.订单处理完成:"+re);
            rabbitTemplate.convertAndSend("finish",re);
        }).start();
    }
  1. 线程2:接收完成订单,并响应请求。
@Slf4j
@Component
@RabbitListener(queues = "finish")
public class OrderFinishThread {

    @Autowired
    private DeferredResultHolder deferredResultHolder;

    @RabbitHandler
    public void process(String re) {
        new Thread(()->{
            log.info("5.监听到完成的订单2:"+re);
            deferredResultHolder.getMap().get(re).setResult("订单成功");
        }).start();
    }
}

使用jmeter对Callable和DeferredResult异步进行简单的测试
并发100个请求
Callable

DeferredResult

posted on 2019-08-18 22:51  丶心  阅读(...)  评论(... 编辑 收藏

统计