前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot几个注解MockMvcWireMockSwagger2@JsonViewHibernate Validator异常处理拦截方式上传下载异步处理RESTSpring Security

SpringBoot几个注解MockMvcWireMockSwagger2@JsonViewHibernate Validator异常处理拦截方式上传下载异步处理RESTSpring Security

作者头像
spilledyear
发布2018-10-09 14:55:39
2K1
发布2018-10-09 14:55:39
举报
文章被收录于专栏:小白鼠

几个注解

某博客

@ConditionalOnMissingBean

只有特定名称或者类型的Bean(通过@ConditionalOnMissingBean修饰)不存在于BeanFactory中时才创建某个Bean

代码语言:javascript
复制
// 只有BeanFactory中没有 imageValidateCodeGenerator这个Bean时才创建
@Bean
@ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
public ValidateCodeGenerator imageValidateCodeGenerator() {
    ImageCodeGenerator codeGenerator = new ImageCodeGenerator(); 
    codeGenerator.setSecurityProperties(securityProperties);
    return codeGenerator;
}
@ConditionalOnBean

和@ConditionalOnMissingBean对应,当BeanFactory中存在某个时才创建

@ConditionalOnClass

类加载器中存在对应的类就执行

@ConditionalOnMissingClass

与@ConditionalOnClass作用一样,条件相反,类加载器中不存在对应的类才执行

有一种东西叫依赖查找,不知道听过没有

代码语言:javascript
复制
@Autowired
private Map<String,DemoService> demoServiceMap;

Spring会将DemoService类型的Bean的名字作为key,对象作为value封装进入Map。同理,还可以使用List的方式

MockMvc

为什么要使用测试?可以避免启动内置的web容器,速度会快很多。

添加依赖

代码语言:javascript
复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

两个关键注解

代码语言:javascript
复制
// 表示以SpringRunner来执行测试用例
@RunWith(SpringRunner.class) 
// 声明当前类为一个测试用例
@SpringBootTest
public class UserControllerTest {
}

WireMock

可以认为WireMock是一个单独的服务器,用来模拟一些数据,可以通过代码控制。

下载WireMock

WrieMock下载

启动WireMock
代码语言:javascript
复制
java -jar wiremock-standalone-2.18.0.jar

启动之后就可以直接给前端或者APP使用了,让它单独在服务器上运行就可以了。至于需要什么样的接口,则是在我们的应用中通过代码来控制

添加依赖
代码语言:javascript
复制
<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock</artifactId>
</dependency>
为WireMock定义接口
代码语言:javascript
复制
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.removeAllMappings;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;

import java.io.IOException;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.io.ClassPathResource;
public class MockServer {

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        // 8062是指刚刚启动的WireMock的端口
        configureFor(8062);
        // 清空之前的缓存,相当于是每次启动的时候都刷新接口
        removeAllMappings();

        mock("/order/1", "01");
        mock("/order/2", "02");
    }

    private static void mock(String url, String file) throws IOException {
        ClassPathResource resource = new ClassPathResource("mock/response/" + file + ".txt");
        String content = StringUtils.join(FileUtils.readLines(resource.getFile(), "UTF-8").toArray(), "\n");
        stubFor(get(urlPathEqualTo(url)).willReturn(aResponse().withBody(content).withStatus(200)));
    }

}

Swagger2

代码语言:javascript
复制
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.2.2</version>
</dependency>

添加一个配置类

代码语言:javascript
复制
@Configuration
@EnableSwagger2
public class Swagger {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.hand.hap.cloud.hdip"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("ServiceName Api")
                .description("ServiceName Api Description")
                .termsOfServiceUrl("localhost:8080")
                .contact("spilledyear")
                .version("1.0")
                .build();
    }
}

@JsonView

这个用于控制返回dto中的哪些字段

代码语言:javascript
复制
public class User {
    
    public interface UserSimpleView {}
    public interface UserDetailView extends UserSimpleView {}
    
    private String id;
    private String username;
    private String password;

    @JsonView(UserSimpleView.class)
    public String getUsername() {
        return username;
    }

    @JsonView(UserDetailView.class)
    public String getPassword() {
        return password;
    }
}

在上面这段代码种,定义了两个JsonView:UserDetailView 和 UserSimpleView,其中UserSimpleView 继承了 UserSimpleView, 说明UserSimpleView返回的json中除了包含自己定义的password字段,还可以返回username字段

定义好了之后,接下来就可以直接在Controller中使用了, 以下返回的json串中将仅包含name属性

代码语言:javascript
复制
@GetMapping
@JsonView(User.UserSimpleView.class)
@ApiOperation(value = "用户查询服务")
public List<User> query(UserQueryCondition condition, @PageableDefault(page = 2, size = 17, sort = "username,asc") Pageable pageable) {

    List<User> users = new ArrayList<>();
    users.add(new User());
    users.add(new User());
    users.add(new User());
    return users;            
}

用起来感觉有点麻烦,看情况使用吧。

Hibernate Validator

用于数据校验!比如在一些字段上添加一些注解,然后通过@Valid 和 BindingResult 使用

代码语言:javascript
复制
public class User {
    @NotBlank(message = "密码不能为空")
    private String password;
}

    
@PutMapping("/{id:\\d+}")
public User update(@Valid @RequestBody User user, BindingResult errors) {
    user.setId("1");
    return user;
}

如果封装的那些注解不能满足需求,可以自定义注解

代码语言:javascript
复制
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
// validatedBy = MyConstraintValidator.class 表示你的校验逻辑在MyConstraintValidator类中
@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyConstraint {
    
    String message();

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

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

}

自定义校验逻辑

代码语言:javascript
复制
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> {

    @Autowired
    private HelloService helloService;
    
        // 初始化时候的逻辑
    @Override
    public void initialize(MyConstraint constraintAnnotation) {
        System.out.println("my validator init");
    }

        // 校验逻辑
    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        helloService.greeting("tom");
        System.out.println(value);
        return true;
    }
}

使用的时候,只需要添加到字段上面就可以了

代码语言:javascript
复制
public class User {
    @MyConstraint(message = "这是一个测试")
    @ApiModelProperty(value = "用户名")
    private String username;
}

异常处理

浏览器发请求返回html;非浏览器发请求返回Json

代码语言:javascript
复制
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {

     //      // 返回html
    @RequestMapping(produces = {"text/html"})
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response){
        HttpStatus status = this.getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        return modelAndView == null ? new ModelAndView("error", model) : modelAndView;
    }

     // 返回json
    @RequestMapping
    @ResponseBody
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = this.getStatus(request);
        return new ResponseEntity(body, status);
    }
}
修改Springboot中默认异常html界面

注意目录结构,在这里面弄进行覆盖

修改Springboot中默认异常json

定义一个异常

代码语言:javascript
复制
public class UserNotExistException extends RuntimeException {
    private static final long serialVersionUID = -6112780192479692859L;
    private String id;
    
    public UserNotExistException(String id) {
        super("user not exist");
        this.id = id;
    }

    public String getId() {
        return id;
    }

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

定义一个异常处理通知

代码语言:javascript
复制
@ControllerAdvice
public class ControllerExceptionHandler {
        
        // 这里面定义UserNotExistException异常返回的内特容
    @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;
    }
}

拦截方式

可以通过 Filter、Interceptor、Aspect 进行拦截

过滤器Filter

让一个Filter在 Springboot中生效有两种

  1. 通过@Component注解
代码语言:javascript
复制
@Component
public class TimeFilter implements Filter {
    @Override
    public void destroy() {
        System.out.println("time filter destroy");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("time filter start");
        long start = new Date().getTime();
        chain.doFilter(request, response);
        System.out.println("time filter 耗时:"+ (new Date().getTime() - start));
        System.out.println("time filter finish");
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {
        System.out.println("time filter init");
    }
}
  1. 通过配置类。比如你想让第三方框架中的某个Filter生效,这时候无法声明@Component注解
代码语言:javascript
复制
public class TimeFilter implements Filter {
    @Override
    public void destroy() {
        System.out.println("time filter destroy");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("time filter start");
        long start = new Date().getTime();
        chain.doFilter(request, response);
        System.out.println("time filter 耗时:"+ (new Date().getTime() - start));
        System.out.println("time filter finish");
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {
        System.out.println("time filter init");
    }
}

@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("/*");
        registrationBean.setUrlPatterns(urls);
        
        return registrationBean;
    }
}
拦截器Interceptor

先定义一个Interceptor,注意,直接这样是不能生效的,还需要配置

代码语言:javascript
复制
@Component
public class TimeInterceptor implements HandlerInterceptor {
    // 执行目标方法前,该方法的返回值决定接下来的代码是否执行,比如 Controller中的方法、postHandle
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        System.out.println("preHandle");
        
        System.out.println(((HandlerMethod)handler).getBean().getClass().getName());
        System.out.println(((HandlerMethod)handler).getMethod().getName());
        
        request.setAttribute("startTime", new Date().getTime());
        return true;
    }

    // 抛异常不执行, Controller中的方法刚执行完就执行这个方法
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle");
        Long start = (Long) request.getAttribute("startTime");
        System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start));

    }

    // 必定会执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        System.out.println("afterCompletion");
        Long start = (Long) request.getAttribute("startTime");
        System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start));
        System.out.println("ex is "+ex);

    }

}

配置让该Interceptor生效

代码语言:javascript
复制
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    @SuppressWarnings("unused")
    @Autowired
    private TimeInterceptor timeInterceptor;

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

}
切面Aspect

这其实是属于SpringAOP的内容了。相对于前两个,这种方式可以在拦截的时候拿到目标方法中的参数值

添加依赖

代码语言:javascript
复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

定义一个切面

代码语言:javascript
复制
@Aspect
@Component
public class TimeAspect {
    
    @Around("execution(* com.imooc.web.controller.UserController.*(..))")
    public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
        
        System.out.println("time aspect start");
        
        Object[] args = pjp.getArgs();
        for (Object arg : args) {
            System.out.println("arg is "+arg);
        }
        
        long start = new Date().getTime();
        
        // 执行目标方法
        Object object = pjp.proceed();
        
        System.out.println("time aspect 耗时:"+ (new Date().getTime() - start));
        
        System.out.println("time aspect end");
        
        return object;
    }

}

执行顺序:Filter --> Interceptor --> Advice --> Controller

上传下载

Springboot处理文件上传下载,实际项目中文件上传可能仅仅是提交文件信息,而文件交由专用服务器处理

文件上传

测试代码

代码语言:javascript
复制
@Test
public void whenUploadSuccess() throws Exception {
    String result = mockMvc.perform(fileUpload("/file")
            .file(new MockMultipartFile("file", "test.txt", "multipart/form-data", "hello upload".getBytes("UTF-8"))))
            .andExpect(status().isOk())
            .andReturn().getResponse().getContentAsString();
    System.out.println(result);
}

对应Controller逻辑

代码语言:javascript
复制
@PostMapping
public FileInfo upload(MultipartFile file) throws Exception {
    String folder = "./";

    System.out.println(file.getName());
    System.out.println(file.getOriginalFilename());
    System.out.println(file.getSize());

    File localFile = new File(folder, new Date().getTime() + ".txt");

    file.transferTo(localFile);

    return new FileInfo(localFile.getAbsolutePath());
}
文件下载
代码语言:javascript
复制
@GetMapping("/{id}")
public void download(@PathVariable String id, HttpServletRequest request, HttpServletResponse response) throws Exception {
    String folder = "./";
    try (InputStream 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();
    } 
}

异步处理REST

异步请求在Springboot中的应用

Runable
代码语言:javascript
复制
@RequestMapping("/order")
public DeferredResult<String> order() throws Exception {
    logger.info("主线程开始");

    Callable<String> result = new Callable<String>() {
        @Override
        public String call() throws Exception {
            logger.info("副线程开始");
            Thread.sleep(1000);
            logger.info("副线程返回");
            return "success";
        }
    };
}
DeferredResult

image.png

DeferredResult用于两个线程间的交互:比如请求线程、返回线程

代码语言:javascript
复制
@Autowired
private MockQueue mockQueue;

@Autowired
private DeferredResultHolder deferredResultHolder;

private Logger logger = LoggerFactory.getLogger(getClass());

@RequestMapping("/order")
public DeferredResult<String> order() throws Exception {
    logger.info("主线程开始");
    // 生成随机订单号
    String orderNumber = RandomStringUtils.randomNumeric(8);
    // 模拟消息队列发送消息
    mockQueue.setPlaceOrder(orderNumber);

    DeferredResult<String> result = new DeferredResult<>();
    deferredResultHolder.getMap().put(orderNumber, result);

    return result;
}
代码语言:javascript
复制
@Component
public class DeferredResultHolder {
    
    private Map<String, DeferredResult<String>> map = new HashMap<String, DeferredResult<String>>();

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

    public void setMap(Map<String, DeferredResult<String>> map) {
        this.map = map;
    }
    
}
代码语言:javascript
复制
@Component
public class QueueListener implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    private MockQueue mockQueue;

    @Autowired
    private DeferredResultHolder deferredResultHolder;
    
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        new Thread(() -> {
            while (true) {

                if (StringUtils.isNotBlank(mockQueue.getCompleteOrder())) {
                    
                    String orderNumber = mockQueue.getCompleteOrder();
                    logger.info("返回订单处理结果:"+orderNumber);
                    deferredResultHolder.getMap().get(orderNumber).setResult("place order success");
                    mockQueue.setCompleteOrder(null);
                    
                }else{
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        }).start();
    }
}
异步配置

主要是和异步有关的一些配置,比如异步情况下的拦截器配置

代码语言:javascript
复制
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        configurer.registerCallableInterceptors(xxx);
        ......
    }
}

Spring Security

返回html还是Json

非常非常常用的场景,后台写了一个接口,比如说登录成功之后,如果是在本系统,可能是直接返回一个界面;如果是前后端分离架构、或者是app应用,这时候需要返回一个json字符串,这就要求后台接口根据不同的清空返回不同的内容,如果是html请i去,就返回界面,如果不是html请求,就返回Json

代码语言:javascript
复制
@RestController
public class BrowserSecurityController {
    private RequestCache requestCache = new HttpSessionRequestCache();

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    @RequestMapping(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        SavedRequest savedRequest = requestCache.getRequest(request, response);

        if (savedRequest != null) {
            String targetUrl = savedRequest.getRedirectUrl();
            logger.info("引发跳转的请求是:" + targetUrl);
            if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
                redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
            }
        }

        return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页");
    }
}


// 用于读取配置文件imooc.security开头的属性,然后放到 BrowserProperties对象中
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
    private BrowserProperties browser = new BrowserProperties();
    
    public BrowserProperties getBrowser() {
        return browser;
    }
    public void setBrowser(BrowserProperties browser) {
        this.browser = browser;
    }
}

// 如果一个配置类开启 配置文件的读取
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
验证码

在UsernamePasswordAuthenticateFilter 过滤器之前添加一个过滤器,即 验证码过滤器。大致思路,生成 验证码,存在session中,然后在过滤器中校验

代码语言:javascript
复制
// 前端关键代码,/code/image 即使对应Controller请求路径
<tr>
    <td>图形验证码:</td>
    <td>
        <input type="text" name="imageCode">
        <img src="/code/image?width=200">
    </td>
</tr>


// 在配置类中开启 /code/image 访问
.authorizeRequests().antMatchers("/code/image");

// 编写过滤器
public class ValidateCodeFilter extends OncePerRequestFilter{
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        ValidateCodeType type = getValidateCodeType(request);
        if (type != null) {
            logger.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type);
            try {
                validateCodeProcessorHolder.findValidateCodeProcessor(type)
                        .validate(new ServletWebRequest(request, response));
                logger.info("验证码校验通过");
            } catch (ValidateCodeException exception) {
                authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
                return;
            }
        }

        chain.doFilter(request, response);

    }
}

// 将该过滤器添加到 UsernamePasswordAuthenticateFilter 前面
ValidateCodeFilter  valicateCodeFilter = new ValidateCodeFilter();
http.addFilterBefore(valicateCodeFilter, UsernamePasswordAuthenticateFilter.class)
RemberMe
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.09.19 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 几个注解
  • MockMvc
  • WireMock
  • Swagger2
  • @JsonView
  • Hibernate Validator
  • 异常处理
  • 拦截方式
  • 上传下载
  • 异步处理REST
  • Spring Security
相关产品与服务
验证码
腾讯云新一代行为验证码(Captcha),基于十道安全栅栏, 为网页、App、小程序开发者打造立体、全面的人机验证。最大程度保护注册登录、活动秒杀、点赞发帖、数据保护等各大场景下业务安全的同时,提供更精细化的用户体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档