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

几个注解

某博客

@ConditionalOnMissingBean

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

// 只有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作用一样,条件相反,类加载器中不存在对应的类才执行

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

@Autowired
private Map<String,DemoService> demoServiceMap;

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

MockMvc

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

添加依赖

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

两个关键注解

// 表示以SpringRunner来执行测试用例
@RunWith(SpringRunner.class) 
// 声明当前类为一个测试用例
@SpringBootTest
public class UserControllerTest {
}

WireMock

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

下载WireMock

WrieMock下载

启动WireMock

java -jar wiremock-standalone-2.18.0.jar

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

添加依赖

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock</artifactId>
</dependency>

为WireMock定义接口

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

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

添加一个配置类

@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中的哪些字段

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属性

@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 使用

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;
}

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

@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 { };

}

自定义校验逻辑

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;
    }
}

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

public class User {
    @MyConstraint(message = "这是一个测试")
    @ApiModelProperty(value = "用户名")
    private String username;
}

异常处理

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

@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

定义一个异常

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;
    }
}

定义一个异常处理通知

@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注解
@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注解
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,注意,直接这样是不能生效的,还需要配置

@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生效

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    @SuppressWarnings("unused")
    @Autowired
    private TimeInterceptor timeInterceptor;

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

}

切面Aspect

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

添加依赖

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

定义一个切面

@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处理文件上传下载,实际项目中文件上传可能仅仅是提交文件信息,而文件交由专用服务器处理

文件上传

测试代码

@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逻辑

@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());
}

文件下载

@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

@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用于两个线程间的交互:比如请求线程、返回线程

@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;
}
@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;
    }
    
}
@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();
    }
}

异步配置

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

@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

@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中,然后在过滤器中校验

// 前端关键代码,/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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏一个会写诗的程序员的博客

8.4 Spring Boot集成Kotlin混合Java开发

本章介绍Spring Boot集成Kotlin混合Java开发一个完整的spring boot应用:Restfeel,一个企业级的Rest API接口测试平台(...

2822
来自专栏菩提树下的杨过

java调用.net asmx / wcf

一、先用asmx与wcf写二个.net web service: 1.1 asmx web服务:asmx-service.asmx.cs 1 using Sy...

2295
来自专栏菩提树下的杨过

dubbox 增加google-gprc/protobuf支持

好久没写东西了,今年实在太忙,基本都在搞业务开发,晚上来补一篇,作为今年的收官博客。google-rpc 正式发布以来,受到了不少人的关注,这么知名的rpc框架...

9998
来自专栏微服务那些事儿

微服务的异常处理

不加班的周末,整理了一下项目上的异常处理方案,和小伙伴们共享,里面不成熟的代码或解决方式.QAQ,评论区走起

1.2K6
来自专栏happyJared

Spring Boot中读取配置属性的几种方式

  本文介绍Spring Boot中读取配置属性的几种方式,项目示例中用到的application.yml和application.properties定义如下...

1.3K2
来自专栏Golang语言社区

Golang Stub初体验

序言 对于领域对象的UT测试来说,基础设施层(infra)的操作函数都应该被打桩。对于Golang来说,大家通常会想到GoMock。GoMock是由Golang...

4179
来自专栏SpringBoot 核心技术

第三十七章:基于SpringBoot架构以及参数装载完成接口安全认证

47110
来自专栏微信公众号:Java团长

自己手写一个SpringMVC框架

前端框架很多,但没有一个框架称霸,后端框架现在Spring已经完成大一统。所以学习Spring是Java程序员的必修课。

952
来自专栏Android 研究

Retrofit解析7之相关类解析

上篇文章讲解了Call接口、CallAdapter接口、Callback接口、Converter接口、Platform类、ExecutorCallAdapter...

1251
来自专栏精讲JAVA

接口方法上的注解无法被 @Aspect 声明的切面拦截的原因分析

在Spring中使用MyBatis的Mapper接口自动生成时,用一个自定义的注解标记在Mapper接口的方法中,再利用@Aspect定义一个切面,拦截这个注解...

2922

扫码关注云+社区

领取腾讯云代金券