SpringBoot

作者: spilledyear | 来源:发表于2018-09-19 11:55 被阅读29次

    几个注解

    某博客

    @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

    相关文章

      网友评论

        本文标题:SpringBoot

        本文链接:https://www.haomeiwen.com/subject/ifvxwftx.html