JavaEE

JavaWeb了解之SpringBoot篇2

2022-11-18  本文已影响0人  平安喜乐698
目录
  1. 配置SpringMVC
  2. 拦截器
  3. 异常处理
  4. 注册Web原生组件(Servlet、Filter、Listener)
  5. JDBC(切换数据源、整合MyBatis)

1. 配置SpringMVC

SpringBoot抛弃了传统的xml配置方式,使用@Configuration注解标注的配置类(替代xml配置文件)来对SpringMVC进行配置。

SpringBoot通过WebMvcAutoConfiguration类为SpringMVC提供了大量默认配置。

可以通过在实现WebMvcConfigurer接口(SpringBoot2.x之前是已废弃的WebMvcConfigurerAdapter抽象类)的配置类(使用@Configuration注解标注的类)中实现对应的接口方法【常用方法见SpringMVC篇】来覆盖对应的默认配置(如:拦截器、格式化程序、视图控制器)。

如果配置类中还添加了@EnableWebMvc注解,则表示全面接管SpringMVC配置(默认的配置全部失效,如:不能再直接访问静态资源文件了除非进行配置 等)。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    // 解决跨域问题
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 映射路径
                .allowedOrigins("*")  
                .allowedMethods("*")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
    // 资源路径映射
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        String os = System.getProperty("os.name");
        if (os.toLowerCase().startsWith("win")) { // windos系统
            registry.addResourceHandler("/img/avatorImages/**")
                    .addResourceLocations("file:" + Constants.RESOURCE_WIN_PATH + "\\img\\avatorImages\\");
        } else { // MAC、Linux系统
            registry.addResourceHandler("/img/avatorImages/**")
                    .addResourceLocations("file:" + Constants.RESOURCE_MAC_PATH + "/img/avatorImages/");
        }
    }
}

2. 拦截器(用于:登陆校验、权限验证、乱码解决、性能监控、异常处理)

步骤:
  1. 定义拦截器(创建普通类,并实现HandlerInterceptor接口的以下方法)。
    1. 在控制器请求方法之前执行(返回值表示是否继续向下执行,false则中断后续操作) 
      boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    2. 在控制器请求方法之后解析视图之前执行(对请求域中的模型和视图做进一步修改)
      void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
    3. 在视图渲染结束后执行(资源清理、记录日志)
      void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
  2. 注册拦截器。
    创建一个实现了WebMvcConfigurer接口的配置类(使用了@Configuration注解的类),重写addInterceptors()方法,在该方法中调用registry.addInterceptor()方法将自定义的拦截器注册到容器中。
  3. 指定拦截规则(在addInterceptors()方法中指定)
    1. addPathPatterns方法:用于指定拦截路径(如:拦截路径为“/**”,表示拦截所有请求,包括对静态资源的请求)。
    2. excludePathPatterns方法:用于排除拦截路径(即指定不需要被拦截器拦截的请求)。

===》 1. 定义拦截器
  在com.sst.cx.componet包下创建LoginInterceptor.java拦截器类,对登陆进行拦截:

package com.sst.cx.componet;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object loginUser = request.getSession().getAttribute("loginUser");
        if (loginUser == null) {
            // 未登录,返回登陆页
            request.setAttribute("msg", "您没有权限进行此操作,请先登陆!");
            request.getRequestDispatcher("/index.html").forward(request, response);
            return false;
        } else {
            // 放行
            return true;
        }
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle执行", modelAndView);
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion执行异常", ex);
    }
}

===》2. 注册拦截器
  在配置类MyMvcConfig中,重写addInterceptors方法注册拦截器:

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    ......
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor());
    }
}

===》3. 指定拦截规则
  在addInterceptors()方法中指定拦截器的拦截规则:

@Slf4j
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    ......
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        log.info("注册拦截器");
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") // 拦截所有请求,包括静态资源文件
                .excludePathPatterns("/", "/login", "/index.html", "/user/login", "/css/**", "/images/**", "/js/**", "/fonts/**"); // 放行登录页,登陆操作,静态资源
    }
}

===》4. 测试(实现登陆功能)
1. 将AdminEx后台管理系统模板(https://gitee.com/coderdlg/adminex下载)中的index.html改名为main.html,并复制到templates目录下。

2. 创建User.java(com.sst.cx.domain包下)
package com.sst.cx.domain;
public class User {
    private String username;
    private Object password;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public Object getPassword() {
        return password;
    }
    public void setPassword(Object password) {
        this.password = password;
    }
}

3. 创建LoginController.java(com.sst.cx.controller包下) ,添加处理登陆请求的doLogin()方法 :
package com.sst.cx.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpSession;
import java.util.Map;
import com.sst.cx.domain.User;
@Slf4j  // 使用该注解后在该类中可直接使用log对象
@Controller
public class LoginController {
    @RequestMapping("/user/login")
    public String doLogin(User user, Map<String, Object> map, HttpSession session) {
        if (user != null && StringUtils.hasText(user.getUsername()) && "123456".equals(user.getPassword())) {
            session.setAttribute("loginUser", user);
            log.info("登陆成功,用户名:" + user.getUsername());
            // 防止重复提交使用重定向
            return "redirect:/main.html";
        } else {
            map.put("msg", "用户名或密码错误");
            log.error("登陆失败");
            return "login";
        }
    }
    /*
    @RequestMapping("/main.html")
    public String mainPage(){
        return "main";
    }*/
}
pom.xml文件中需添加lombok依赖
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

4. 在配置类的addViewControllers方法中添加视图映射:
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 当访问 “/” 或 “/index.html” 时跳转到 login.html
        registry.addViewController("/").setViewName("login");
        registry.addViewController("/index.html").setViewName("login");
        // 当访问 main.html 时跳转到 main.html
        registry.addViewController("/main.html").setViewName("main");
    }
    ......
}

5. 在login.html的适当位置添加以下代码(显示错误信息)
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

6. 启动SpringBoot
  在未登录的情况下,直接通过“http://localhost:8080/main.html”访问主页,会提示没权限。
  在登陆页输入错误的用户名或密码会提示错误。
  在登陆页输入任意用户名和123456密码,则进入主页main.html。

3. 异常处理

  1. 默认异常处理

项目运行时会出现各种异常,需要一个统一的异常处理机制,来保证发生异常时客户端能进行友好的提示。SpringBoot提供了一个默认的异常处理机制:程序异常后,SpringBoot会自动识别客户端类型(浏览器客户端:返回一个html错误页面;App客户端/Postman:返回一个json)。
SpringBoot是通过ErrorMvcAutoConfiguration配置类对异常处理提供了自动配置,该配置类向容器中注入了以下4个组件:

1. ErrorPageCustomizer(定制错误页面的响应规则)
  发生异常后,默认将请求转发到“/error”上,然后交给BasicErrorController进行处理。
  @Bean
  public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
    return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
  }

ErrorPageCustomizer是通过registerErrorPages()方法来注册错误页面的响应规则。
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
  // 发生异常后,默认将请求转发到/errror上
  ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
  // 注册错误页面
  errorPageRegistry.addErrorPages(errorPage);
}
2. BasicErrorController(错误控制器,出错时根据客户端类型调用errorHtml或error来处理错误)
  处理默认的“/error”请求。
  @Bean
  @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
  public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
                                                 ObjectProvider<ErrorViewResolver> errorViewResolvers) {
    return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
            errorViewResolvers.orderedStream().collect(Collectors.toList()));
  }

BasicErrorController的定义如下:
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    ......
    // 处理浏览器客户端的请求发生的异常,生成html页面来展示异常信息。
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        // 获取错误状态码
        HttpStatus status = getStatus(request);
        // getErrorAttributes根据错误信息来封装一些model数据 用于页面显示
        Map<String, Object> model = Collections
                .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        // 为响应对象设置错误状态码
        response.setStatus(status.value());
        // 调用 resolveErrorView() 方法,使用错误视图解析器生成 ModelAndView 对象(包含错误页面地址和页面内容)
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }
    // 处理App客户端的请求发生的异常,产生JSON格式的数据展示异常信息
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        return new ResponseEntity<>(body, status);
    }
    ......
}

处理浏览器请求异常时,在errorHtml()方法中调用了父类(AbstractErrorController)的resolveErrorView()方法寻找对应的错误页:
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,Map<String, Object> model) {
    // 获取容器中所有的ErrorViewResolver错误视图解析器(包括 DefaultErrorViewResolver)并分别调用它们的resolveErrorView()方法根据异常信息解析为对应的错误视图页面。
    for (ErrorViewResolver resolver : this.errorViewResolvers) {
        ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
        if (modelAndView != null) {
            return modelAndView;
        }
    }
    return null;
}
3. DefaultErrorViewResolver(仅用于处理浏览器请求异常)
  默认的错误视图解析器,将异常信息解析到相应的错误视图上。
  @Bean
  @ConditionalOnBean(DispatcherServlet.class)
  @ConditionalOnMissingBean(ErrorViewResolver.class)
  DefaultErrorViewResolver conventionErrorViewResolver() {
      return new DefaultErrorViewResolver(this.applicationContext, this.resources);
  }

DefaultErrorViewResolver的定义如下:
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
    private static final Map<HttpStatus.Series, String> SERIES_VIEWS;
    static {
        Map<HttpStatus.Series, String> views = new EnumMap<>(HttpStatus.Series.class);
        views.put(Series.CLIENT_ERROR, "4xx");
        views.put(Series.SERVER_ERROR, "5xx");
        SERIES_VIEWS = Collections.unmodifiableMap(views);
    }
    ......
    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        // 尝试以错误状态码作为错误页面名进行解析(如:4xx解析为4xx.html)
        ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
        }
        return modelAndView;
    }
    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        // 错误模板页面,例如 error/404、error/4xx、error/500、error/5xx
        String errorViewName = "error/" + viewName;
        // 当模板引擎可以解析这些模板页面时,就用模板引擎解析
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
                this.applicationContext);
        if (provider != null) {
            // 在模板能够解析到模板页面的情况下,返回 errorViewName 指定的视图
            return new ModelAndView(errorViewName, model);
        }
        // 若模板引擎不能解析,则去静态资源文件夹下查找 errorViewName 对应的页面
        return resolveResource(errorViewName, model);
    }
    private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
        // 遍历所有静态资源文件夹
        for (String location : this.resources.getStaticLocations()) {
            try {
                Resource resource = this.applicationContext.getResource(location);
                // 静态资源文件夹下的错误页面,例如error/404.html、error/4xx.html、error/500.html、error/5xx.html
                resource = resource.createRelative(viewName + ".html");
                // 若静态资源文件夹下存在以上错误页面,则直接返回
                if (resource.exists()) {
                    return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
                }
            } catch (Exception ex) {
            }
        }
        return null;
    }
    ......
}

从源码可知,DefaultErrorViewResolver解析异常信息的步骤如下:
  1. 尝试以状态码作为错误页面名来解析(即使用模板引擎解析error/status,会在templates目录下寻找),如:error/404 会在templates/error/目录下寻找404.html文件。如果解析成功(即找到对应的文件),则将视图和数据封装成ModelAndView并返回。
  2. 如果解析失败,则依次从各个静态资源文件夹中查找error/status.html,若找到则返回。
  3. 如果没有找到,将错误状态码(如:404)转换为4xx,重新执行前两步。 
  4. 如果还是没找到,则使用SpringBoot默认的错误页面(Whitelabel Error Page)。
4. DefaultErrorAttributes
  用于在页面中共享异常信息 或 将异常信息返回给客户端。
  @Bean
  @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
  public DefaultErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes();
  }

DefaultErrorAttributes可以从请求中获取异常或错误信息并封装为一个Map对象。
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
    ...
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
        if (!options.isIncluded(Include.EXCEPTION)) {
            errorAttributes.remove("exception");
        }
        if (!options.isIncluded(Include.STACK_TRACE)) {
            errorAttributes.remove("trace");
        }
        if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
            errorAttributes.remove("message");
        }
        if (!options.isIncluded(Include.BINDING_ERRORS)) {
            errorAttributes.remove("errors");
        }
        return errorAttributes;
    }
    private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap<>();
        errorAttributes.put("timestamp", new Date());
        addStatus(errorAttributes, webRequest);
        addErrorDetails(errorAttributes, webRequest, includeStackTrace);
        addPath(errorAttributes, webRequest);
        return errorAttributes;
    }
    ...
}

BasicErrorController中调用了DefaultErrorAttributes的getErrorAttributes()方法获取错误或异常信息(Map对象,可以直接在页面或JSON中获取)。
Map对象主要包含以下属性:
  1. timestamp(时间戳)
  2. status(错误状态码)
  3. error(错误的提示)
  4. exception(导致请求处理失败的异常对象)
  5. message(错误/异常消息)
  6. trace(错误/异常栈信息)
  7. path(错误/异常抛出时所请求的URL路径)
  例
    {
      "timestamp": "2015-10-28T07:05:29.885+00:00",
      "status": 404,
      "error": "Not Found",
      "message": "No message available",
      "path": "/main.html"
    }
  1. 全局异常处理
异常处理的3种方式:
  1. 基于请求转发(真正的全局异常处理)
    BasicExceptionController(SpringBoot默认处理异常方式)
  2. 基于过滤器(处理不了过滤器之前抛出的异常)
    Filter:全部需要手写代码(需要考虑到所有可能的情况,容易出问题)。
  3. 基于异常处理器(处理不了过滤器抛出的异常)
    @ExceptionHandler(不推荐)
    @ControllerAdvice+@ExceptionHandler
    SimpleMappingExceptionResolver(不推荐)
    HandlerExceptionResolver

全局异常处理的实现方案:
  1. 基于请求转发(推荐)
  2. 基于过滤器(不推荐)
  3. 基于异常处理器+请求转发补充(推荐)
    由异常处理器处理大多数自定义的异常,剩下的交给异常处理控制器。
    @ControllerAdvice+@ExceptionHandler+BasicExceptionController
    ...
  4. 基于异常处理器+过滤器补充(不推荐)
    @ControllerAdvice+@ExceptionHandler+Filter(无法处理过滤器之前抛出的异常)
    ...
  使用3、4两种方式时会多次捕获抛出的异常,导致重复写入响应信息。解决:使用包装的Response:ResponseWrapper(在Controller中第一次捕捉到异常时,异常处理器写入的响应信息写入到Response代理对象;在BasicExceptionController/Filter中第二次捕捉到异常时,响应信息写入到Response原对象中)。
1. BasicExceptionController
  程序出现异常后,SpringBoot就会请求/error的url(即基于请求转发来处理异常),BasicExceptionController类会处理/error请求,并跳转到默认显示异常的页面(可在resources/templates/目录下创建error.html/jsp来自定义异常页面)来展示异常信息。
  获取到的异常消息有限(timestamp、status、error、message、path等),通常项目中会使用自己定义的code、msg、data,业务错误码code、错误信息msg 不好获取。

可创建自定义异常处理控制器类(继承自BasicErrorController)来自定义异常处理。
@RestController
public class MyErrorController extends BasicErrorController {
    public MyErrorController() {
        super(new DefaultErrorAttributes(), new ErrorProperties());
    }
    // produces属性:设置返回的数据类型为application/json
    @Override
    @RequestMapping(value = "", produces = {MediaType.APPLICATION_JSON_VALUE})
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        // 获取错误信息
        Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        String code = body.get("status").toString();
        String message = body.get("message").toString();
        return new ResponseEntity(ApiUtil.fail(message), HttpStatus.OK);
    }
}
2. Filter
  使用自定义过滤器进行异常处理时,应放置在过滤链的第一个位置。
===》ResponseWrapper
  避免多次抛出异常导致的响应json信息问题,异常处理器捕捉,过滤器又捕捉
public class ResponseWrapper extends HttpServletResponseWrapper {
    private ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    private PrintWriter printWriter = new PrintWriter(outputStream);
    public ResponseWrapper(HttpServletResponse response) {
        super(response);
    }
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return new ServletOutputStream() {
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setWriteListener(WriteListener writeListener) {

            }
            @Override
            public void write(int b) throws IOException {
                outputStream.write(b);
            }
            @Override
            public void write(byte[] b) throws IOException {
                outputStream.write(b);
            }
            @Override
            public void write(byte[] b, int off, int len) throws IOException {
                outputStream.write(b, off, len);
            }
            @Override
            public void flush() throws IOException {
                outputStream.flush();
            }
        };
    }
    @Override
    public PrintWriter getWriter() throws IOException {
        return printWriter;
    }
    public void flush(){
        try {
            printWriter.flush();
            printWriter.close();
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public byte[] getContent() {
        flush();
        return outputStream.toByteArray();
    }
}

===》创建自定义过滤器
public class ExceptionFilter extends OncePerRequestFilter {
/*
使用异常处理器处理异常---1
    @Autowired
    private HandlerExceptionResolver handlerExceptionResolver;
*/
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException,IOException {
        try {
            ResponseWrapper responseWrapper = new ResponseWrapper(httpServletResponse);
            filterChain.doFilter(httpServletRequest, responseWrapper);
            // 读取响应内容
            byte[] bytes = responseWrapper.getContent();
            // 可对响应内容进行修改等操作。。。
            // 模拟Filter抛出异常:throw new IOException("Filter error");
            // 内容重新写入原响应对象中
            httpServletResponse.getOutputStream().write(bytes);
        } catch (Exception e) {
/*
使用异常处理器处理异常---2  (将catch中的代码替换为)
handlerExceptionResolver.resolveException(httpServletRequest, httpServletResponse, null, e);
*/
            httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
            httpServletResponse.getOutputStream().write(JSON.toJSONString(ApiUtil.fail(e.getMessage())).getBytes());
        }
    }
}

===》在@Configuration修饰的配置类中,添加(放在第一个位置)
    // 自定义异常过滤器
    @Bean
    ExceptionFilter exceptionFilter() {
        return new ExceptionFilter();  
    }
    // 
    @Bean
    public FilterRegistrationBean exceptionFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(exceptionFilter());
        registration.setName("exceptionFilter");
        // 此处尽量小,要比其他Filter靠前
        registration.setOrder(-1);
        return registration;
    }
3. @ExceptionHandler
  只能作用于:使用@ExceptionHandler注解修饰的Controller方法。
  通过HandlerExceptionResolverComposite异常处理器中的ExceptionHandlerExceptionResolver异常处理器处理。

例:
@RestController
public class TestController {
    @GetMapping("test9")
    public FundInfo test9() throws Exception {
        throw new Exception("test9 error");
    }
    @GetMapping("test10")
    public FundInfo test10() throws Exception {
        throw new IOException("test10 error");
    }
    @ExceptionHandler(Exception.class)
    public ApiResult exceptionHandler(Exception e) {
        return ApiUtil.custom(500, e.getMessage());
    }
}
4. @ControllerAdvice+@ExceptionHandler
  作用于Controller,以及DispatcherServlet.doDispatch方法中DispatcherServlet.processDispatchResult方法之前捕捉到的所有异常,包括:拦截器、参数绑定(参数解析、参数转换、参数校验)、控制器、返回值处理等模块抛出的异常。
  通过HandlerExceptionResolverComposite异常处理器中的ExceptionHandlerExceptionResolver异常处理器处理。

例:
@SuppressWarnings("ALL")
@RestControllerAdvice  // 是@ControllerAdvice的扩展,等价于@ControllerAdvice+@ResponseBody
public class MyGlobalExceptionHandler {
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.OK)  // 指定成功执行时的http状态码(出错后,返回的不再是这个状态码)。
    public ApiResult bindException(HttpServletRequest request,
                                   HttpServletResponse response,
                                   BindException exception) {
        return ApiUtil.fail(exception.getBindingResult().getFieldError().getDefaultMessage());
    }
    @ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.OK)
    public ApiResult methodArgumentNotValidException(HttpServletRequest request,
                                                     HttpServletResponse response,
                                                     MethodArgumentNotValidException exception) {
        return ApiUtil.fail(exception.getBindingResult().getFieldError().getDefaultMessage());
    }
    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseStatus(HttpStatus.OK)
    public ApiResult methodArgumentNotValidException(HttpServletRequest request,
                                                     HttpServletResponse response,
                                                     MissingServletRequestParameterException exception) {
        return ApiUtil.fail(exception.getMessage());
    }
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.OK)
    public ApiResult methodArgumentNotValidException(HttpServletRequest request,
                                                     HttpServletResponse response,
                                                     ConstraintViolationException exception) {
        System.out.println(exception.getLocalizedMessage());
        Iterator<ConstraintViolation<?>> iterator = exception.getConstraintViolations().iterator();
        if (iterator.hasNext()) {
            ConstraintViolationImpl next = (ConstraintViolationImpl)iterator.next();
            return ApiUtil.fail(next.getMessage());
        }
        return ApiUtil.fail(exception.getMessage());
    }
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.OK)
    public ApiResult exception(HttpServletRequest request,
                               HttpServletResponse response,
                               Exception exception) {
        return ApiUtil.fail(exception.getMessage());
    }
}

/*
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ......省略代码......
    try {
        ModelAndView mv = null;
        Exception dispatchException = null;
        try {
            ......省略代码......
            mappedHandler = getHandler(processedRequest);
            ......省略代码......
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
            ......省略代码......
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }
            ......省略代码......
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }
            ......省略代码......
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        ......省略代码......
    }
    catch (Throwable err) {
        ......省略代码......
    }
    finally {
        ......省略代码......
    }
}
*/
5. SimpleMappingExceptionResolver
  在@Configuration注解修饰的配置类中,向容器中注入SimpleMappingExceptionResolver。
  不能得到具体的异常信息,且返回的是视图。
  通过SimpleMappingExceptionResolver异常处理器处理。

例:
@Configuration
public class GlobalExceptionConfig {
   @Bean
   public SimpleMappingExceptionResolver getSimpleMappingExceptionResolver(){
       SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
       // 异常的类型(必须是全名),要跳转的视图名称
       Properties mappings = new Properties();
       mappings.put("java.lang.ArithmeticException", "error1");
       mappings.put("java.lang.NullPointerException", "error1");
       mappings.put("java.lang.Exception", "error1");
       mappings.put("java.io.IOException", "error1");
       // 设置异常与视图的映射信息
       resolver.setExceptionMappings(mappings);
       return resolver;
   }
}
6. HandlerExceptionResolver
  实现HandlerExceptionResolver接口,在@Configuration注解修饰的配置类中,向容器中注入该Resolver。

例
// AbstractHandlerExceptionResolver类实现了HandlerExceptionResolver接口。
public class MyExceptionResolver extends AbstractHandlerExceptionResolver {
    // 异常解析器的顺序(数值越小优先级越高)
    @Override
    public int getOrder() {
        return -999999;
    }
    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        try {
            response.getWriter().write(JSON.toJSONString(ApiUtil.fail(ex.getMessage())));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

4. 注册Web原生组件(Servlet、Filter、Listener)

SpringBoot以Jar包方式部署(因此:没有webapp目录、没有web.xml文件),无法像以前一样通过web.xml配置来使用Servlet、Filter、Listener。

SpringBoot提供了2种方式注册Web原生组件:
  1. 通过组件扫描注册(推荐)
    使用@ServletComponentScan注解(只能标记在启动类或配置类上)注册组件(会扫描标记了@WebServlet、@WebFilter 和@WebListener注解的组件类,并注册到容器中)    
  2. 使用RegistrationBean注册(繁琐)
    不再需要使用@WebServlet、@WebListener、@WebListener、@ServletComponentScan注解。
    在配置类中给RegistrationBean抽象类(3个实现类:ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean分别用来注册Servlet、Filter、Listener到Servlet容器中)添加@Bean注解添加到Spring容器中。

例(通过组件扫描注册)

===》1. 使用@WebServlet注解声明一个Servlet:
package com.sst.cx.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(name = "myServlet", urlPatterns = "/myServlet")
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter writer = resp.getWriter();
        writer.write("Spring Boot Servlet");
        writer.close();
    }
}

===》2. 使用@WebFilter注解声明一个Filter:
package com.sst.cx.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = ("/myServlet"))
public class MyFiler implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("MyFiler 初始化");
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("MyFiler doFilter");
        chain.doFilter(request, response);
    }
    @Override
    public void destroy() {
        System.out.println("MyFiler 销毁");
    }
}

===》3. 使用@WebListener注解声明一个Listener:
package com.sst.cx.Listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener
public class MyListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("MyListener 监听到 ServletContext 初始化");
    }
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("MyListener 监听到 ServletContext 销毁");
    }
}

===》4. 在启动类上使用@ServletComponentScan注解:
package com.sst.cx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan
@SpringBootApplication
public class SpringBootServletApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootServletApplication.class, args);
    }
}
浏览器中访问:http://localhost:8080/myServlet

例(使用RegistrationBean注册)

===》1. 创建Servlet
package com.sst.cx.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter writer = resp.getWriter();
        writer.write("Spring Boot Servlet");
        writer.close();
    }
}

===》2. 创建Filter
package com.sst.cx.filter;
import javax.servlet.*;
import java.io.IOException;
public class MyFiler implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("MyFiler 初始化");
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("MyFiler doFilter");
        chain.doFilter(request, response);
    }
    @Override
    public void destroy() {
        System.out.println("MyFiler 销毁");
    }
}

===》3. 创建Listener
package com.sst.cx.Listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class MyListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("MyListener 监听到 ServletContext 初始化");
    }
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("MyListener 监听到 ServletContext 销毁");
    }
}

===》4. 创建MyConfig配置类
  使用@Bean注解将ServletRegistrationBean、FilterRegistrationBean、 ServletListenerRegistrationBean(分别用于注册Servlet、Filter、Listener)添加到Spring容器中。
package com.sst.cx.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import com.sst.cx.Listener.MyListener;
import com.sst.cx.filter.MyFiler;
import com.sst.cx.MyServlet;
@Configuration
public class MyConfig {
    // 注册 servlet
    @Bean
    public ServletRegistrationBean servletRegistrationBean() {
        MyServlet myServlet = new MyServlet();
        return new ServletRegistrationBean(myServlet, "/myServlet");
    }
    // 注册过滤器
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        MyFiler myFiler = new MyFiler();
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFiler);
        // 注册该过滤器需要过滤的url
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/myServlet"));
        return filterRegistrationBean;
    }
    // 注册监听器
    @Bean
    public ServletListenerRegistrationBean servletListenerRegistrationBean() {
        MyListener myListener = new MyListener();
        return new ServletListenerRegistrationBean(myListener);
    }
}

在浏览器中访问http://localhost:8080/myServlet

5. JDBC

SpringBoot通过大量默认配置,简化对数据访问层(无论是SQL关系型数据库 还是NOSQL非关系型数据库)的操作。

步骤:
  1. 在pom.xml中导入JDBCstarter、数据库驱动实现。
    <!--导入JDBC starter(默认会引入了一个数据源:HikariCP)-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jdbc</artifactId>
    </dependency>
    <!--导入数据库驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
  2. 配置数据源(在配置文件application.properties/yml中)
    #数据源连接参数
    spring:
      datasource:
        username: root
        password: 12345678
        url: jdbc:mysql://127.0.0.1:3306/Test
        driver-class-name: com.mysql.cj.jdbc.Driver
  3. 测试
    SpringBoot提供了JdbcTemplate类(对JDBC进行了封装,轻量级数据访问工具)并进行了大量默认配置,使用@Autowired注解将其注入到容器中。
    例:
      package com.sst.cx;
      import org.junit.jupiter.api.Test;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.springframework.jdbc.core.JdbcTemplate;
      import javax.sql.DataSource;
      import java.sql.SQLException;
      @SpringBootTest
      class SpringBootJdbcApplicationTests {
          // 数据源组件
          @Autowired
          DataSource dataSource;
          // 用于访问数据库的组件
          @Autowired
          JdbcTemplate jdbcTemplate;
          @Test
          void contextLoads() throws SQLException {
              System.out.println("默认数据源为:" + dataSource.getClass());
              System.out.println("数据库连接实例:" + dataSource.getConnection());
              // 访问数据库
              Integer i = jdbcTemplate.queryForObject("SELECT count(*) from user", Integer.class);
              System.out.println("user 表中共有" + i + "条数据。");
          }
      }

数据源配置原理

===》SpringBoot2.x之前默认使用Tomcat数据源,之后默认使用HikariCP数据源。
  org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration部分源码(2.x之前)如下:
abstract class DataSourceConfiguration {    
  ...
  // Tomcat数据源配置  
  // 如果当前环境中加载了括号中的类,则条件成立。
  @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)    
  // 如果在properties配置文件中配置了spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource则条件成立。matchIfMissing属性值为true表示即使没有配置,条件仍然成立。  
  @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.tomcat.jdbc.pool.DataSource", matchIfMissing = true)    
  //上面两个条件都成立时,系统会加载Tomcat数据源配置  
  static class Tomcat extends DataSourceConfiguration {   
    @Bean    
    // 自动配置绑定(将properties文件中以spring.datasource.tomcat开头的属性值注入到方法返回的bean)
    @ConfigurationProperties(prefix = "spring.datasource.tomcat")    
    // 创建Tomcat数据源并返回
    public org.apache.tomcat.jdbc.pool.DataSource dataSource(DataSourceProperties properties) {    
    }    
  }    

  // Hikari数据源配置  
  @ConditionalOnClass(HikariDataSource.class)    
  @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true)    
  static class Hikari extends DataSourceConfiguration {    
    @Bean    
    @ConfigurationProperties(prefix = "spring.datasource.hikari")    
    // 创建Hikari数据源并返回
    public HikariDataSource dataSource(DataSourceProperties properties) {    
      ...
    }    
  }  
  ...
}    
如果在SpringBoot项目中添加了JDBCstarter,从上述源码可知会默认使用Tomcat数据源(因为Tomcat数据源排在前面,会先判断其是否满足条件,满足条件后会自动加载Tomcat的class)。
改为Hikari数据源,需进行如下配置:
  1. 在pom.xml中添加
  <dependency>   
    <groupId>com.zaxxer</groupId>  
    <artifactId>HikariCP</artifactId>  
  </dependency> 
  2. 在application.properties中添加(会使Tomcat数据源的条件1不成立)
  spring.datasource.type=com.zaxxer.hikari.HikariDataSource  
  2. 或者 在JDBCstarter中去除tomcat的class
  <dependency>    
    <groupId>org.springframework.boot</groupId>    
    <artifactId>spring-boot-starter-jdbc</artifactId>    
    <exclusions>    
        <exclusion>    
            <artifactId>tomcat-jdbc</artifactId>    
            <groupId>org.apache.tomcat</groupId>    
        </exclusion>    
    </exclusions>    
  </dependency>  
  3. 查看当前数据源
  System.out.println(dataSource.getClass());  
  1. 改为Druid数据源

SpringBoot2.x中默认使用HikariCP数据源(目前市面上性能最好的数据源产品)来获取数据库连接,对数据库进行增删改查等操作。
但在实际开发过程中,企业往往更青睐于Druid数据源(目前国内使用范围最广的数据源产品)。

Druid(阿里巴巴推出的一款开源的高性能数据源产品)
  1. 支持所有JDBC兼容的数据库(Oracle、MySQL、SQL Server、H2等)。
  2. 不仅结合了C3P0、DBCP、PROXOOL等数据源产品的优点,同时还加入了强大的监控功能。通过Druid的监控功能,可以实时观察数据库连接池和SQL的运行情况,帮助开发者及时排查出系统中存在的问题。
  3. 不是SpringBoot提供的技术,属于第三方技术。

SpringBoot项目中整合Druid:
    方式1. 自定义整合Druid(繁琐) 
      根据Druid官方文档和自身需求,通过手动创建Druid数据源的方式,将Druid整合到 SpringBoot项目中。
    方式2. 通过starter整合Druid。
      目前为止SpringBoot官方只对Hikari、Tomcat、Dbcp2、OracleUcp四种数据源产品提供了自动配置支持。为此,阿里官方提供了DruidSpringBootStarter(在SpringBoot项目中轻松整合Druid)。

方式1(自定义整合Druid)

1. 在pom.xml中引入Druid依赖、JDBC starter、MySql数据库驱动
<!-- 导入 JDBC starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<!-- 导入 数据库驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- 导入 Druid依赖 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.6</version>
</dependency>
2. 创建数据源
  查看源码可知Hikari类有一个@ConditionalOnMissingBean(DataSource.class)注解,表示容器中没有数据源类时(即项目中没有指定数据源时)会使用HikariCP作为其默认数据源,向容器中添加数据源后(即项目中指定数据源后)则使用该数据源而不再使用默认的HikariCP。

===》创建MyDataSourceConfig配置类(com.sst.cx.config包下)
  将Druid数据源对象添加到容器中。
package com.sst.cx.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.sql.DataSource;
import java.sql.SQLException;
@Configuration
public class MyDataSourceConfig implements WebMvcConfigurer {
    // 向容器中添加Druid数据源
    // 方式1:使用@ConfigurationProperties注解设置数据源属性(会将配置文件中spring.datasource开头的配置与数据源中的属性进行绑定)。
/*
在application.yml配置文件添加如下内容(会与Druid数据源中的属性进行绑定):
#数据源连接信息
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/bianchengbang_jdbc
    driver-class-name: com.mysql.cj.jdbc.Driver
*/
    @ConfigurationProperties("spring.datasource")
    @Bean
    public DataSource dataSource() throws SQLException {
        DruidDataSource druidDataSource = new DruidDataSource();
        // 方式2:通过代码设置数据源属性(不建议)。
/*
        druidDataSource.setUrl("jdbc:mysql://127.0.0.1:3306/bianchengbang_jdbc");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("root");
        druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
*/
        return druidDataSource;
    }
}
===》测试
  验证数据源类型是否为Druid,是否能正常获取数据库连接、访问数据库。
package com.sst.cx;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.sql.SQLException;
@SpringBootTest
class SpringBootAdminexApplicationTests {
    // 数据源组件
    @Autowired
    DataSource dataSource;
    // 用于访问数据库的组件
    @Autowired
    JdbcTemplate jdbcTemplate;
    @Test
    void contextLoads() throws SQLException {
        System.out.println("默认数据源为:" + dataSource.getClass());
        System.out.println("数据库连接实例:" + dataSource.getConnection());
        // 访问数据库
        Integer i = jdbcTemplate.queryForObject("SELECT count(*) from `user`", Integer.class);
        System.out.println("user 表中共有" + i + "条数据。");
    }
}
3. 开启Druid内置监控页面
  Druid内置了StatViewServlet,可用来开启Druid的内置监控页面功能(提供监控信息展示的html页面),展示Druid的统计信息(提供监控信息的JSON API)。StatViewServlet是一个标准的javax.servlet.http.HttpServlet,想要开启Druid的内置监控页面,需要将该Servlet配置在WEB-INF/web.xml中: 
    <servlet>
      <servlet-name>DruidStatView</servlet-name>
      <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>DruidStatView</servlet-name>
        <url-pattern>/druid/*</url-pattern>
    </servlet-mapping>
  但SpringBoot项目中是没有WEB-INF/web.xml的,可以在配置类中通过ServletRegistrationBean将StatViewServlet注册到容器中,来开启Druid的内置监控页面。

===》在MyDataSourceConfig配置类中将StatViewServlet注入到容器中,来开启Druid内置监控页面功能。
@Bean
public ServletRegistrationBean statViewServlet() {
    StatViewServlet statViewServlet = new StatViewServlet();
    // 向容器中注入StatViewServlet,并将其路径映射设置为/druid/*
    ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(statViewServlet, "/druid/*");
    // 配置监控页面访问的账号和密码(选配)
    servletRegistrationBean.addInitParameter("loginUsername", "admin");
    servletRegistrationBean.addInitParameter("loginPassword", "123456");
    return servletRegistrationBean;
}
启动SpringBoot,
在浏览器中访问http://localhost:8080/druid 即可访问Druid的内置监控页面的登陆页,在登陆页输入框内输入用户名(loginUsername)和密码(loginPassword),点击Sign in按钮访问监控页面。
4. 开启SQL监控
  Druid内置了StatFilter(别名:stat),可用来开启Druid的SQL监控功能(对SQL进行监控)。
  Spring项目中开启DruidSQL监控:
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
      ...
      <property name="filters" value="stat" />
    </bean>
  SpringBoot项目只需在dataSource的Bean中添加一个取值为stat的filters属性就可以开启DruidSQL监控。

===》给dataSourceBean添加filters属性
@ConfigurationProperties("spring.datasource")
@Bean
public DataSource dataSource() throws SQLException {
    DruidDataSource druidDataSource = new DruidDataSource();
    // 设置filters属性值为stat,开启SQL监控
    druidDataSource.setFilters("stat");
    return druidDataSource; 
}
===》测试(验证Druid SQL监控是否开启)
  在IndexController中添加一个查询数据库的方法。
@Controller
public class IndexController {
    @Autowired
    JdbcTemplate jdbcTemplate;
    // 访问"/testSql",访问数据库
    @ResponseBody
    @GetMapping("/testSql")
    public String testSql() {
        String SQL = "SELECT count(*) from `user`";
        Integer integer = jdbcTemplate.queryForObject(SQL, Integer.class);
        return integer.toString();
    }
}
启动SpringBoot,
在浏览器中访问http://localhost:8080/testSql。访问Druid的内置监控页面,切换到SQL监控选项卡,可以看到Druid SQL监控已经开启。
5. 开启防火墙
  Druid内置了WallFilter(别名:wall),可用来开启防火墙功能(防御SQL注入攻击)。
  Spring项目中开启防火墙:
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
      ... 
      <!-- 可结合其他Filter一起使用:wall,stat -->
      <property name="filters" value="wall" />  
    </bean>
  SpringBoot项目只需在dataSource的Bean中添加一个取值为wall的filters属性就可以开启防火墙。

===》给dataSourceBean添加filters属性
@ConfigurationProperties("spring.datasource")
@Bean
public DataSource dataSource() throws SQLException {
    DruidDataSource druidDataSource = new DruidDataSource();
    // 同时开启sql监控(stat)、防火墙(wall),中间用逗号隔开。
    druidDataSource.setFilters("stat,wall");
    return druidDataSource;
}
启动Spring Boot,
在浏览器中访问http://localhost:8080/testSql。访问Druid的内置监控页面,切换到SQL防火墙选项卡,可以看到Druid防火墙已经开启。
6. 开启Web-JDBC关联监控
  Druid内置了WebStatFilter,可用来监控与采集web-jdbc关联监控的数据。
  根据官方文档,只需将WebStatFilter配置在WEB-INF/web.xml中即可开启:
    <filter>
        <filter-name>DruidWebStatFilter</filter-name>
        <filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
        <init-param>
            <param-name>exclusions</param-name>
            <param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>DruidWebStatFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
  SpringBoot项目中可以在配置类中通过FilterRegistrationBean将WebStatFilter注入到容器中来开启Druid的Web-JDBC关联监控。

===》MyDataSourceConfig配置类中将WebStatFilter注入到容器中
// 向容器中添加WebStatFilter
@Bean
public FilterRegistrationBean druidWebStatFilter() {
    WebStatFilter webStatFilter = new WebStatFilter();
    FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(webStatFilter);
    // 监控所有的访问
    filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
    // 监控访问不包括以下路径
    filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
    return filterRegistrationBean;
}
启动SpringBoot,
在浏览器中访问http://localhost:8080/testSql,访问AdminEx系统的任意页面,然后再访问Druid的内置监控页面。
  切换到Web应用选项卡,可以看到Druid的Web监控已经开启。
  切换到URI监控选项卡,可以看到URI监控、Session监控已经开启。

方式2(通过starter整合Druid)

1. 在pom.xml中引入DruidSpringBootStarter依赖
<!-- 添加druid starter -->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.1.17</version>
</dependency>

2. 修改默认配置
在application.properties/yml(内容较多时推荐yml)配置文件中修改:
  1. JDBC通用配置
    数据库用户名、密码、URL、驱动类
  2. Druid 数据源连接池配置
  3. Druid 监控配置
    Druid内置监控页面、Web-JDBC关联监控、Spring监控、
  4. Druid 内置Filter配置
    StatFilter
    WallFilter
    ConfigFilter
    EncodingConvertFilter
    Slf4jLogFilter
    Log4jFilter
    Log4j2Filter
    CommonsLogFilter
    可以通过spring.datasource.druid.filters=stat,wall ... 的方式来启用相应的内置Filter。
    可以通过spring.datasource.druid.filter.* 修改默认配置。
例

===》1. JDBC通用配置
spring:
  datasource:                                          
    username: root                               #用户名                                                           
    password: root                               #密码                                                  
    url: jdbc:mysql://127.0.0.1:3306/Test        #数据库url                                                             
    driver-class-name: com.mysql.cj.jdbc.Driver  #数据库驱动类                                  

===》2. Druid 数据源连接池配置
spring:
  datasource:
    druid:
      initial-size: 5                                                                 #初始化连接大小
      min-idle: 5                                                                     #最小连接池数量
      max-active: 20                                                                  #最大连接池数量
      max-wait: 60000                                                                 #获取连接时最大等待时间,单位毫秒
      time-between-eviction-runs-millis: 60000                                        #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      min-evictable-idle-time-millis: 300000                                          #配置一个连接在池中最小生存的时间,单位是毫秒
      validation-query: SELECT 1 FROM DUAL                                            #测试连接
      test-while-idle: true                                                           #申请连接的时候检测,建议配置为true,不影响性能,并且保证安全性
      test-on-borrow: false                                                           #获取连接时执行检测,建议关闭,影响性能
      test-on-return: false                                                           #归还连接时执行检测,建议关闭,影响性能
      pool-prepared-statements: false                                                 #是否开启PSCache,PSCache对支持游标的数据库性能提升巨大,oracle建议开启,mysql下建议关闭
      max-pool-prepared-statement-per-connection-size: 20                             #开启poolPreparedStatements后生效
      filters: stat,wall                                                              #配置扩展插件,常用的插件有=>stat:监控统计  wall:防御sql注入
      connection-properties: 'druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000' #通过connectProperties属性来打开mergeSql功能;慢SQL记录

===》3. Druid 监控配置
spring:
  datasource:
    druid:
      # StatViewServlet配置,说明请参考Druid Wiki,配置_StatViewServlet配置
      stat-view-servlet:
        enabled: true                                                                 #是否开启内置监控页面,默认值为 false
        url-pattern: '/druid/*'                                                       #StatViewServlet 的映射路径,即内置监控页面的访问地址
        reset-enable: true                                                            #是否启用重置按钮
        login-username: admin                                                         #内置监控页面的登录页用户名 username
        login-password: admin                                                         #内置监控页面的登录页密码 password
      # WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter
      web-stat-filter:
        enabled: true                                                                 #是否开启内置监控中的 Web-jdbc 关联监控的数据
        url-pattern: '/*'                                                             #匹配路径
        exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'                     #排除路径
        session-stat-enable: true                                                     #是否监控session
      # Spring监控配置,说明请参考Druid Github Wiki,配置_Druid和Spring关联监控配置
      aop-patterns: net.biancheng.www.*                                               #Spring监控AOP切入点,如x.y.z.abc.*,配置多个英文逗号分隔

===》4. Druid 内置Filter配置
  需要先将对应Filter的enabled设置为 true,否则内置Filter的配置不会生效。
spring:
  datasource:
    druid:
     # 对配置已开启的 filters 即 stat(sql 监控)  wall(防火墙)
      filter:
        #配置StatFilter (SQL监控配置)
        stat:
          enabled: true                                                               #开启 SQL 监控
          slow-sql-millis: 1000                                                       #慢查询
          log-slow-sql: true                                                          #记录慢查询 SQL
        #配置WallFilter (防火墙配置)
        wall:
          enabled: true                                                               #开启防火墙
          config:
            update-allow: true                                                        #允许更新操作
            drop-table-allow: false                                                   #禁止删表操作
            insert-allow:  true                                                       #允许插入操作
            delete-allow: true                                                        #删除数据操作
  1. 整合MyBatis

使用MyBatis时必须配置数据源连接参数(URL、用户名、密码、驱动类)。

步骤:
  1. 在pom.xml中引入mybatis依赖:
    <!-- 引入mybatis依赖 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
  2. 修改MyBatis默认配置(在application.properties/yml配置文件中)
      mybatis:
        #指定mapper.xml的位置
        mapper-locations: classpath:mybatis/mapper/*.xml
        #指定实体类的包路径(在mapper.xml中就不用写类的完全限定名了)
        type-aliases-package: com.sst.cx.bean
        configuration:
          #是否开启驼峰命名法(默认开启)
          map-underscore-to-camel-case: true  
  3. 测试
===》创建数据库表,根据数据库表自动生成实体类、Mapper映射文件、Mapper接口。
===》创建UserService、UserServiceImpl
===》在LoginController中使用UserService的方法访问数据库。
package com.sst.cx.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpSession;
import java.util.Map;
import com.sst.cx.bean.User;
import com.sst.cx.service.UserService;
@Slf4j
@Controller
public class LoginController {
    @Autowired
    UserService userService;
    @RequestMapping("/user/login")
    public String doLogin(User user, Map<String, Object> map, HttpSession session) {
        // 从数据库中查询用户信息
        User loginUser = userService.getByUserNameAndPassword(user);
        if (loginUser != null) {
            session.setAttribute("loginUser", loginUser);
            log.info("登陆成功,用户名:" + loginUser.getUserName());
            // 防止重复提交使用重定向
            return "redirect:/main.html";
        } else {
            map.put("msg", "用户名或密码错误");
            log.error("登陆失败");
            return "login";
        }
    }
}

可能遇到的错误

1. 主启动类需要放置在groupId对应包路径下,否则访问页面404.
上一篇下一篇

猜你喜欢

热点阅读