springboot随笔

1、springboot中classpath表示什么

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9EEH5lLE-1682653820318)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660923261991.png)]

2、error:jdk isnt specified for module

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gNTvlmSj-1682653820319)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660923503020.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LnpM66Ky-1682653820319)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660923523798.png)]

3、/和/*的区别

< url-pattern > / </ url-pattern > 不会匹配到.jsp, 只针对我们编写的请求;即:.jsp 不会进入spring的 DispatcherServlet类 。< url-pattern > /* </ url-pattern > 会匹配 *.jsp,会出现返回 jsp视图 时再次进入spring的DispatcherServlet 类,导致找不到对应的controller所以报404错。

4、ModelAndView类

设置ModelAndView对象 , 根据view的名称 , 和视图解析器跳到指定的页面 .

页面 : {视图解析器前缀} + viewName +{视图解析器后缀}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6BqdKu26-1682653820319)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660963210286.png)]

5、ServletAPI

通过设置ServletAPI , 不需要视图解析器 .

1、通过HttpServletResponse进行输出

2、通过HttpServletResponse实现重定向

3、通过HttpServletResponse实现转发

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8kVA1cG8-1682653820319)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660963281769.png)]

6、通过SpringMVC实现转发和重定向

1、没有视图解析器的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B4oIS6k3-1682653820320)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660963434815.png)]

2、有需要视图解析器的情况下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1PU1uZvW-1682653820320)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660963506171.png)]

7、当前端送过来的数据和处理器参数名称一致,可以直接接收

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UTmUwIyr-1682653820320)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660963638939.png)]

8、当前端送过来的数据和处理器参数名称不一致,通过@RequestParam(“username”)来指定

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iHGj6Ww6-1682653820320)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660963722525.png)]

9、前端送过来的数据是一个对象的话,处理器的参数名必须和域传过来的名称一致

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kHqwg6Gy-1682653820320)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660963861767.png)]

10、把后端数据传递到前端,方式一:通过ModelAndView

ModelAndView不仅可以传递处理器控制方法的处理数据到结果页面,还可以指定跳转的页面。并且ModelAndView需要我们自己new。

这个类中是通过两个方法:

mv.addObject("msg","ControllerTest1"); //这个是通过key-value的方式绑定数据
//这个方法是用来设置跳转页面,由DispatcherServlet通过ViewResolver解析器解析
mv.setViewName("test"); 
mv.setViewNanme("redirect:/index.html");

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aSau1vEJ-1682653820320)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660964368121.png)]

11、把后端数据传递到前端,方式二:通过ModelMap

ModelMap只能传递处理器控制方法的处理数据给结果页面,不会进行业务的寻址,所以ModelMa的返回值一定是URL地址,即只能通过控制器方法的返回值来设置要跳转的URL或者页面。

ModelMap的每一次请求底层都帮我们创建好了,不需要我们自己new;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JziopgUb-1682653820321)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660964947972.png)]

12、把后端数据传递到前端,方式三:通过Model

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZlFotvNO-1682653820321)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660965285986.png)]

13、如何自定义拦截器呢–> 想要自定义拦截器,必须实现 HandlerInterceptor 接口。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o251bZib-1682653820321)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1660965979223.png)]

14、拦截器中/admin/*和/admin/**的区别

<!--/** 包括路径及其子路径-->      
<!--/admin/* 拦截的是/admin/add等等这种 , /admin/add/user不会被拦截-->   
<!--/admin/** 拦截的是/admin/下的所有-->

15、文件上传(比较特殊:对应的类是CommonsMultipartResolver,但是实例化的时候,规定对象名必须是 multipartResolver ,否则报错)

也就是前端上传过来的文件,到了服务器是对应一个封装的类:CommonsMultipartFile

<!--文件上传配置-->
<bean id="multipartResolver"  class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
   <!-- 请求的编码格式,必须和jSP的pageEncoding属性一致,以便正确读取表单的内容,默认为ISO-8859-1 -->
   <property name="defaultEncoding" value="utf-8"/>
   <!-- 上传文件大小上限,单位为字节(10485760=10M) -->
   <property name="maxUploadSize" value="10485760"/>
   <property name="maxInMemorySize" value="40960"/>
</bean>

文件上传对应有三个常用方法:

CommonsMultipartFile 的 常用方法:

  • String getOriginalFilename():获取上传文件的原名
  • InputStream getInputStream():获取文件流
  • void transferTo(File dest):将上传文件保存到一个目录文件中
package com.kuang.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.commons.CommonsMultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.*;

@Controller
public class FileController {
   //@RequestParam("file") 将name=file控件得到的文件封装成CommonsMultipartFile 对象
   //批量上传CommonsMultipartFile则为数组即可
   @RequestMapping("/upload")
   public String fileUpload(@RequestParam("file") CommonsMultipartFile file , HttpServletRequest request) throws IOException {

       //获取文件名 : file.getOriginalFilename();
       String uploadFileName = file.getOriginalFilename();

       //如果文件名为空,直接回到首页!
       if ("".equals(uploadFileName)){
           return "redirect:/index.jsp";
      }
       System.out.println("上传文件名 : "+uploadFileName);

       //上传路径保存设置
       String path = request.getServletContext().getRealPath("/upload");
       //如果路径不存在,创建一个
       File realPath = new File(path);
       if (!realPath.exists()){
           realPath.mkdir();
      }
       System.out.println("上传文件保存地址:"+realPath);

       InputStream is = file.getInputStream(); //文件输入流
       OutputStream os = new FileOutputStream(new File(realPath,uploadFileName)); //文件输出流

       //读取写出
       int len=0;
       byte[] buffer = new byte[1024];
       while ((len=is.read(buffer))!=-1){
           os.write(buffer,0,len);
           os.flush();
      }
       os.close();
       is.close();
       return "redirect:/index.jsp";
  }
}
/*
* 采用file.Transto 来保存上传的文件
*/
@RequestMapping("/upload2")
public String  fileUpload2(@RequestParam("file") CommonsMultipartFile file, HttpServletRequest request) throws IOException {

   //上传路径保存设置
   String path = request.getServletContext().getRealPath("/upload");
   File realPath = new File(path);
   if (!realPath.exists()){
       realPath.mkdir();
  }
   //上传文件地址
   System.out.println("上传文件保存地址:"+realPath);

   //通过CommonsMultipartFile的方法直接写文件(注意这个时候)
   file.transferTo(new File(realPath +"/"+ file.getOriginalFilename()));

   return "redirect:/index.jsp";
}

16、文件下载

文件下载步骤:

1、设置 response 响应头

2、读取文件 – InputStream

3、写出文件 – OutputStream

4、执行操作

5、关闭流 (先开后关)

@RequestMapping(value="/download")
public String downloads(HttpServletResponse response ,HttpServletRequest request) throws Exception{
   //要下载的图片地址
   String  path = request.getServletContext().getRealPath("/upload");
   String  fileName = "基础语法.jpg";

   //1、设置response 响应头
   response.reset(); //设置页面不缓存,清空buffer
   response.setCharacterEncoding("UTF-8"); //字符编码
   response.setContentType("multipart/form-data"); //二进制传输数据
   //设置响应头
   response.setHeader("Content-Disposition",
           "attachment;fileName="+URLEncoder.encode(fileName, "UTF-8"));

   File file = new File(path,fileName);
   //2、 读取文件--输入流
   InputStream input=new FileInputStream(file);
   //3、 写出文件--输出流
   OutputStream out = response.getOutputStream();

   byte[] buff =new byte[1024];
   int index=0;
   //4、执行 写出操作
   while((index= input.read(buff))!= -1){
       out.write(buff, 0, index);
       out.flush();
  }
   out.close();
   input.close();
   return null;
}
//前端
<a href="/download">点击下载</a>

17、springboot中的父依赖:所有的依赖都被封装在里面

父依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

点进去发现还有一个父依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.2.5.RELEASE</version>
    <relativePath>../../spring-boot-dependencies</relativePath>
</pare

18、springboot的自动配置原理

springboot是通过@SpringbootApplication注解里面的@EnableAutoConfiguration注解来进行自动配置的

自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。 我们以

**HttpEncodingAutoConfiguration(Http编码自动配置)**为例解释自动配置原理;

//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;
@Configuration 

//启动指定类的ConfigurationProperties功能;
  //进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来;
  //并把HttpProperties加入到ioc容器中
@EnableConfigurationProperties({HttpProperties.class}) 

//Spring底层@Conditional注解
  //根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效;
  //这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效
@ConditionalOnWebApplication(
    type = Type.SERVLET
)

//判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;
@ConditionalOnClass({CharacterEncodingFilter.class})

//判断配置文件中是否存在某个配置:spring.http.encoding.enabled;
  //如果不存在,判断也是成立的
  //即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的;
@ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
)

public class HttpEncodingAutoConfiguration {
    //他已经和SpringBoot的配置文件映射了
    private final Encoding properties;
    //只有一个有参构造器的情况下,参数的值就会从容器中拿
    public HttpEncodingAutoConfiguration(HttpProperties properties) {
        this.properties = properties.getEncoding();
    }
    
    //给容器中添加一个组件,这个组件的某些值需要从properties中获取
    @Bean
    @ConditionalOnMissingBean //判断容器没有这个组件?
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
        return filter;
    }
    //。。。。。。。
}

一句话总结 :根据当前不同的条件判断,决定这个配置类是否生效!

  • 一但这个配置类生效;这个配置类就会给容器中添加各种组件;

  • 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;

  • 所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;

  • 配置文件能配置什么就可以参照某个功能对应的这个属性类

//从配置文件中获取指定的值和bean的属性进行绑定  ,就是以这样的方式来绑定获取的
@ConfigurationProperties(prefix = "spring.http") 
public class HttpProperties {
   // .....
}

1、SpringBoot启动会加载大量的自动配置类

2、我们看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;

3、我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)

4、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;

**xxxxAutoConfigurartion类:自动配置类;**给容器中添加组件

xxxxProperties类:封装配置文件中相关属性;

19、springboot利用方法向容器中注册组件

创建的是:ServletRegistrationBean()这个方法

//配置 Druid 监控管理后台的Servlet;
//内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式
@Bean
public ServletRegistrationBean statViewServlet() {
    ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");

    // 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet 
    // 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
    Map<String, String> initParams = new HashMap<>();
    initParams.put("loginUsername", "admin"); //后台管理界面的登录账号
    initParams.put("loginPassword", "123456"); //后台管理界面的登录密码

    //后台允许谁可以访问
    //initParams.put("allow", "localhost"):表示只有本机可以访问
    //initParams.put("allow", ""):为空或者为null时,表示允许所有访问
    initParams.put("allow", "");
    //deny:Druid 后台拒绝谁访问
    //initParams.put("kuangshen", "192.168.1.20");表示禁止此ip访问

    //设置初始化参数
    bean.setInitParameters(initParams);
    return bean;
}

20、cron表达式

//常用表达式例子

  (10/2 * * * * ?   表示每2秒 执行任务

  (10 0/2 * * * ?    表示每2分钟 执行任务

  (10 0 2 1 * ?   表示在每月的1日的凌晨2点调整任务

  (20 15 10 ? * MON-FRI   表示周一到周五每天上午10:15执行作业

  (30 15 10 ? 6L 2002-2006   表示2002-2006年的每个月的最后一个星期五上午10:15执行作

  (40 0 10,14,16 * * ?   每天上午10点,下午2点,4点 

  (50 0/30 9-17 * * ?   朝九晚五工作时间内每半小时 

  (60 0 12 ? * WED    表示每个星期三中午12点 

  (70 0 12 * * ?   每天中午12点触发 

  (80 15 10 ? * *    每天上午10:15触发 

  (90 15 10 * * ?     每天上午10:15触发 

  (100 15 10 * * ?    每天上午10:15触发 

  (110 15 10 * * ? 2005    2005年的每天上午10:15触发 

  (120 * 14 * * ?     在每天下午2点到下午2:59期间的每1分钟触发 

  (130 0/5 14 * * ?    在每天下午2点到下午2:55期间的每5分钟触发 

  (140 0/5 14,18 * * ?     在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 

  (150 0-5 14 * * ?    在每天下午2点到下午2:05期间的每1分钟触发 

  (160 10,44 14 ? 3 WED    每年三月的星期三的下午2:102:44触发 

  (170 15 10 ? * MON-FRI    周一至周五的上午10:15触发 

  (180 15 10 15 * ?    每月15日上午10:15触发 

  (190 15 10 L * ?    每月最后一日的上午10:15触发 

  (200 15 10 ? * 6L    每月的最后一个星期五上午10:15触发 

  (210 15 10 ? * 6L 2002-2005   2002年至2005年的每月的最后一个星期五上午10:15触发 

  (220 15 10 ? * 6#3   每月的第三个星期五上午10:15触发

21、springboot整合过滤器

整合监听器/过滤器和拦截器

在实际开发过程中,经常会碰见一些比如系统启动初始化信息、统计在线人数、在线用户数、过滤敏/高词汇、访问权限控制(URL级别)等业务需求。实现以上的功能,都会或多或少的用到过滤器监听器拦截器

一.SpringBoot整合过滤器Filter
过滤器Filter,是Servlet的的一个实用技术了。可以通过过滤器,对请求进行拦截处理。

1.编写Filter过滤器
1、编写普通Java类实现接口Filter。

​ 2、使用注解@WebFilter标注过滤器类,并配置过滤url。

@WebFilter("/*")// 当前配置拦截所有请求
public class TestFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("过滤器Filter测试执行…………");
        chain.doFilter(request, response);// 放行
    }

}

​ 3、在启动类加入@ServletComponentScan注解
使用@ServletComponentScan注解后,Servlet、Filter、Listener可以直接通过@WebServlet、@WebFilter、@WebListener注解自动注册。

当注册多个过滤器时,无法指定执行顺序,早期使用web.xml配置过滤器时,是可指定执行顺序,但使用注解@WebFilter时,没有顺序这个配置属性。通常情况下,如果对过滤器有特定顺序要求的,我们推荐采用原始方式配置,或者参考测试结果:执行顺序和类名字符排序有关。另外SpringBoot也为解决这个问题,单独提供了一个类FilterRegistrationBean,此类提供setOrder方法,可以为filter设置排序值,让Spring在注册Filter之前排序后再依次注册。

2.解决多过滤器执行顺序问题
1、编写两个/以上Filter,修改Filter的实现(去除注解@WebFilter即可,其他代码无需改动)

​ 2、编写一个config配置类,利用FilterRegistrationBean实现注册过滤器。FilterRegistrationBean是SpringBoot提供的用于注册和解决Filter执行顺序问题的类。注意在类上使用注解@Configuration,在方法上使用注解@Bean。

@Configuration    // 标注为Spring配置beans组件
public class FilterConfig {

    // 注册第一个Filter
    @Bean // 标注为Spring配置bean组件
    public FilterRegistrationBean<Filter> registerFilter1() {
        //通过FilterRegistrationBean实例设置优先级可以生效
        FilterRegistrationBean<Filter> registrationBean = new  FilterRegistrationBean<>();
        // 注册自定义过滤器
        registrationBean.setFilter(new TestFilter1());
        // 设置过滤器的名字<filter-name>
        registrationBean.setName("filter01");
        // 设置过滤器的名字过滤路径<url-partten>
        registrationBean.addUrlPatterns("/*");
        // 设置过滤器优先级:最顶级
        registrationBean.setOrder(1);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean<Filter> registerFilter2() {
        FilterRegistrationBean<Filter> registrationBean = new  FilterRegistrationBean<>();
        // 注册第二个自定义过滤器TestFilter2
        registrationBean.setFilter(new TestFilter2());
        registrationBean.setName("filter02");
        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(5);
        return registrationBean;
    }

}

​ 3、开启启动类,访问测试,查看控制台结果
​ 说明:这种方式可以不使用注解@ServletComponentScan

22、springboot整合监听器Listener

Listnner是servlet规范中定义的一种特殊类。用于监听ServletContext、HttpSession和servletRequest等域对象的创建和销毁事件。监听域对象的属性发生修改的事件。用于在事件发生前、发生后做一些必要的处理。一般是获取在线人数等业务需求。

​ 1、创建普通类实现监听器接口(比较多,我就不一一列出了)

本次案例:创建了

ServletRequest

监听器,实现接口

ServletRequestListnner

@WebListener
@Slf4j  // 该注解等价于Logger  log = new Logger(。。。。)
public class TestListnner implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        log.info("ServletRequest出生…………");
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        log.info("ServletRequest销毁…………");
    }
}


​ 2、在启动类加入@ServletComponentScan注解
使用@ServletComponentScan注解后,Servlet、Filter、Listener可以直接通过@WebServlet、@WebFilter、@WebListener注解自动注册。

23、springboot整合拦截器HandlerInterceptor

​ 1、以上的过滤器、监听器都属于Servlet的API,我们在开发中过滤web请求时,还可以使用Spring提供的拦截器(HandlerInterceptor)进行更加精细的控制。

  1. 编写普通类实现接口HandlerInterceptor。
@Slf4j
public class TestHandlerInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        log.info("preHandle请求访问前,拦截执行……");
        // 返回 false 则请求中断
        return  true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        log.info("postHandle请求访问后,执行……");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        log.info("afterCompletion请求调用完成后回调方法,即在视图渲染完成后回调……");
    }

}

​ 2、WebMvcConfigurerAdapter配置类是spring提供的一种配置方式,采用JavaBean的方式替代传统的基于xml的配置来对spring框架进行自定义的配置,可以自定义一些Handler,Interceptor,ViewResolver,MessageConverter。因此,在spring boot提倡的基于注解的配置,采用“约定大于配置”的风格下,当需要进行自定义的配置时,

①implements WebMvcConfigurer(官方推荐)

②extends WebMvcConfigurationSupport

@Configuration  // 标注为Spring组件
public class HandlerInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加一个实现HandlerInterceptor接口的拦截器实例
        registry.addInterceptor(new TestHandlerInterceptor())
            // 用于设置拦截器的过滤路径规则
            .addPathPatterns("/**")
            // 用于设置不需要拦截的过滤规则
            .excludePathPatterns("/emp/toLogin","/emp/login", "/js/**", "/css/**", "/images/**");
    }
}

24、理解过滤器、拦截器、监听器

img

25、AOP的功能

其实AOP的作用就是在不改动原本代码的基础上为其进行功能增强。

26、在Aop操作中,在通知里如何调用被增强的方法,也即是接入点

通过一个Spring底层写好了的类: ProceedingJionPoint这个类 。

通过调用这个类的proceed()方法,调用的就是原本的那个要被增强的方法, 这个方法的返回值是原本哪个方法的返回值,也即是切入点的返回值,返回值也必须要用Object接收。

27、Aop操作里@Around这个注解,如果原本哪个方法有返回值,那么被增强后的方法也必须要有返回值,返回值一定要是Object类型

28、在Aop操作里,如何获取原始方法的参数呢

方式一:通过Spring封装好了的类:JointPoint,这个类里面的getArgs()这个方法,返回值是Object[]数组。

方式二:通过Spring封装好了的类:ProceedingJointPoint,这个类不仅可以通过getArgs()这个方法获取原始方法的参数,还可以通过proceed()这个方法来调用原始方法,同时接收原始方法的返回值。

29、让Springboot开启某项注解开发功能(Springboot默认只开启了Spring注解开发,像Aop,事务管理,缓存,web的注解开发功能未开启)

在这个注解比较流行的年代里,当我们想要使用spring 的某些功能时只需要加上一行代码就可以了,比如:

  • @EnableAspectJAutoProxy开启AOP, 开启后才能识别@Aspect注解
  • @EnableTransactionManagement开启spring事务管理, 开启后才能识别@Transactional注解
  • @EnableCaching开启spring缓存
  • @EnableWebMvc 开启webMvc注解开发功能

30、Spring中事务回滚原则

Spring中事务回滚(也就是配置了@Transactional注解的方法)只会回滚Error异常和运行时异常, 一旦遇到不是这两种的,事务就不会回滚。比如遇到IOException异常,事务就不会回滚。这个时候的话,就需要设置@Transactional注解里面的rollback属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SmCujf9O-1682653820322)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1661146359677.png)]

31、Spring中事务的传播行为

什么叫事务传播行为?听起来挺高端的,其实很简单。
即然是传播,那么至少有两个东西,才可以发生传播。单体不存在传播这个行为。

事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。

例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。

Spring定义了七种传播行为:

在这里插入图片描述

例如:

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
doSomeThingA();
methodB();
doSomeThingB();
// do something else
}


// 事务属性为REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    // do something
}


main{  
methodA();
} 

img

32、SpringMVC中处理器的请求映射路径设置方式

一般的情况下,我们请求映射路径是设置成: /模块名/映射的功能名称/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NbBnjnbU-1682653820322)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1661148287047.png)]

33、前端送来的参数,处理器(控制器方法)如何接收

形参是普通数据类型的时候,形参直接定义为对应的类型参数来接收

1、如果前端送过来的参数名和处理器形参名一样,那么Springmvc会自动帮我们对应到处理器方法中的形参中。

2、如果前端送过来的参数名和处理器形参名不一样,那么可以在形参前面用@RequestParam(“url中的参数名”)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3rY0dnT7-1682653820322)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1661149437702.png)]

形参是实体类数据类型(即引用数据类型)的时候,形参直接定义成实体类来接收:让前端送过来的数据名和这个实体类的属性名必须要一致,这样的话,SpringMVC就会自动帮我们把值注入到参数中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X7K98Wx9-1682653820322)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1661150132369.png)]

如果形参是集合类型,那么前端送过来的参数名,必须要和处理器方法的形参名一致,而且集合类型的时候,比较特殊,一定得在形参前面加上@RequestParam注解,告诉SpringMVC,前端传过来的数据数据作为参数注入到形参中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5hnstsj7-1682653820323)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1661150151570.png)]

34、get请求和post请求有什么区别、

  • POST请求通过请求体来传递数据,通过param1=value1&param2=value2的键值对形式编码。

  • GET请求通过URL地址参数来传递数据,也就是我们平时看到的URL地址里面“?”后面的所包含的键值对。

  • GET请求发送的只有请求头,没有请求体。POST请求的数据保存在请求体里。

  • 一般情况下,如果前端以GET请求,后端用@RequestParam注解接收参数;如果以POST请求发送,后端一般用@RequestBody注解来接收参数

    一. GET和POST是什么?
    HTTP协议中的两种发送请求的方法,本质上都是在进行TCP连接.

1、get请求一般用来请求获取数据, post请求一般作为发送数据到后台,传递数据,创建数据

2、get请求也可以传参到后台,但是传递的参数则显示在地址栏,安全性低,且参数的长度也有限制(2048字符),post请求则是将传递的参数放在request body,请求体中,不会在地址栏显示,安全性比get请求高,参数没有长度限制

3、get请求刷新浏览器或者回退没有影响, post请求则会重新请求一遍

4、get请求可以被缓存,也会保留在浏览器的历史记录中, post请求不会被缓存,也不好保留在浏览器的历史记录中

5、get请求通常是通过url地址请求, post常见的则是form表单请求

6、get产生一个tcp数据包, post产生两个tcp数据包

7、get产生的URL地址可以被Bookmark,而post不可以

8、get请求会被浏览器主动cache, 而post不会,除非手动设置

9、对参数的数据类型,get只接受ASCII字符,而post没有限制。

10、get比post更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。

GET和POST有一个重大的区别:

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

但是!

  1. GET与POST都有自己的语义,不能随便混用。

  2. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。

  3. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

35、idea里全局搜索快捷键:双击shift

36、idea里如果新建application.yaml后不是绿叶图表的话

需要我们先将yaml文件删掉,需要先将pom文件里面的依赖导入,如何再新建。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gv8CDHrB-1682653820323)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1661409849470.png)]

37、网页上如何查看发送的详细信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZgRGKTxn-1682653820323)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1661421251922.png)]

这样点出来后,还要再次刷新一下页面。这样就会显示数据了。

38、@RequestBody,@RequestParam和@PathVariable注解的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OPUOKACC-1682653820323)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1661421406736.png)]

39、如果maven导入依赖的时候,本地仓库有,但是idea还是报错

那么我们可以先刷新maven,如果刷新后还是报错,那么我们可以把依赖先去掉,如何重启idea,如何再把依赖加进去。如果此时还是报错,那么我们就把本地仓库里面的对应的那个文件夹删掉,如何重新clean—install。

40、idea里如何隐藏掉target,iml等文件,让工程看着清爽

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3rTsqzVy-1682653820323)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1661484615982.png)]

41、rpc的含义是:remote process call : 远程进程调用

42、@Configuration用来在工厂中一次性创建多个对象

这个注解是用来生成第三方组件的,和@Bean注解一起使用。

43、在springboot里文件的上传是通过MultiPart这个类的对象来作为参数接收的。

package com.xing.springbootfileupload.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

@Controller
public class UserController {
    @RequestMapping("/")
    public String welcome(){
        return "index";
    }//这里相当于设置欢迎页

    @RequestMapping("/upload")
    @ResponseBody
    public String upload(String name, MultipartFile phone) throws IOException {//实现文件上传
        System.out.println("上传的用户名为:"+name);
        System.out.println("图像的原始名称为:"+phone.getOriginalFilename());
        System.out.println("上传文件的类型为:"+phone.getContentType());
        saveFile(phone);
        return "文件上传成功";
    }

    public void saveFile(MultipartFile phone) throws IOException {//将文件保存到本地
        String dir="这里写自己要保存图片的绝对路径";//建议这里写resources目录的绝对路径
        File path=new File(dir+"/upload/");
        if(!path.exists()){//如果当前目录不存在
            path.mkdir();
        }
        File file=new File(dir+"/upload/"+phone.getOriginalFilename());
        phone.transferTo(file);//将此图像保存到file本地
    }
}


或者

package com.example.demo.controller;

import com.example.demo.R.Result;
import io.swagger.annotations.Api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;


@Api(value = "文件上传,下载相关功能")
@RestController
public class FileController {
    // 设置固定的日期格式
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    // 将 yml 中的自定义配置注入到这里
    @Value("${gorit.file.root.path}")
    private String filePath;
    // 日志打印
    private Logger log = LoggerFactory.getLogger("FileController");

    // 文件上传 (可以多文件上传)
    @PostMapping("/upload")
    public Result fileUploads(HttpServletRequest request, @RequestParam("file") MultipartFile file) throws IOException {
        // 得到格式化后的日期
        String format = sdf.format(new Date());
        // 获取上传的文件名称
        String fileName = file.getOriginalFilename();
        // 时间 和 日期拼接
        String newFileName = format + "_" + fileName;
        // 得到文件保存的位置以及新文件名
        File dest = new File(filePath + newFileName);
        try {
            // 上传的文件被保存了
            file.transferTo(dest);
            // 打印日志
            log.info("上传成功,当前上传的文件保存在 {}",filePath + newFileName);
            // 自定义返回的统一的 JSON 格式的数据,可以直接返回这个字符串也是可以的。
            return Result.succ("上传成功");
        } catch (IOException e) {
            log.error(e.toString());
        }
        // 待完成 —— 文件类型校验工作
        return Result.fail("上传错误");
    }

}

44、springboot中实现文件的下载

https://blog.csdn.net/aaa58962458/article/details/120764754

 @RequestMapping("/dowload")
  public void dowload(HttpServletResponse response, int id) throws IOException {
    // 通过id查询到这个文件
    Files files = filesService.queryById(id);
    System.out.println("++++++++++"+files);
    // 得到文件的路径
    String Path = ResourceUtils.getURL("classpath:").getPath() + "/static" + files.getPath();
    System.out.println("++++++文件的下载路径" + Path);
    String decode = URLDecoder.decode(Path);
    System.out.println("+++++++++路径" + decode);
    // 通过文件的路径 得到需要下载的文件    为这个文件创建输入流
    FileInputStream in =
        new FileInputStream(new File(URLDecoder.decode(Path), files.getNewfilename()));
    String oldfilename = files.getOldfilename();
    System.out.println(oldfilename);
    // 设置返回值
    response.setHeader(
            "Content-Disposition",
            "attachment;filename=" + URLEncoder.encode(files.getOldfilename(), "UTF-8"));
//    // servlet输出流
    ServletOutputStream os = response.getOutputStream();
    // copy 这个方法将内容按字节从一个InputStream对象复制到一个OutputStream对象,并返回复制的字节数。
    //通过ioutil 对接输入输出流,实现文件下载
    IOUtils.copy(in, os);
    // closeQuietly它将无条件的关闭一个可被关闭的对象而不抛出任何异常。
    // 它也有很多版本去支持关闭所有的InputStream、OutputStream、Reader和Writer。
    IOUtils.closeQuietly(in);
    IOUtils.closeQuietly(os);
    // 设置下载次数+1
    files.setDowncounts(files.getDowncounts() + 1);
    // 更新数据库中的数据
    filesService.update(files);
  }

45、springboot里实现自定义拦截器的方法

1、开发拦截器, 就是自己写一个拦截器类,这个类一定要实现HandlerInteceptor接口

public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        System.out.println("======1=====");
        return true;//返回true 放行  返回false阻止
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {
        System.out.println("=====2=====");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {
        System.out.println("=====3=====");
    }
}

2、注入拦截器, 也就是要写一个类,这个类一定要实现WebMvcConfigurer这个接口,利用这个接口里面的addInteceptor方法

@Configuration
public class WebMvcConfig  implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
      	registry.addInterceptor("拦截器")
                .addPathPatterns("拦截路径")
                .excludePathPatterns("排除路径")
          			.order("指定执行顺序")
    }
}

注意:order用来执行多个拦截器的执行顺序

46、springboot中如果要设置全局(或者定制化的)异常处理,如何设置,通过@ControllerAdvice+@ExceptionHandler(value = Exception.class)来配置

/**
 * 全局异常处理之RestFul
 */
@ControllerAdvice
public class GlobalExceptionHandlerRest {
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ResponseEntity<String> exceptionHandler(Exception e) {
        return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

47、springboot里面的跨域问题

什么情况下属于跨域: 如果地址里面的 协议、域名、端口都相同,则属于同源,否则有一个不同,就属于跨域问题了

方式一:是通过@CrossOrigin注解

@RestController
@RequestMapping("demos")
@CrossOrigin
public class DemoController {
    @GetMapping
    public String demos() {
        System.out.println("========demo=======");
        return "demo ok";
    }
}

然后

@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 1允许任何域名使用
        corsConfiguration.addAllowedHeader("*"); // 2允许任何头
        corsConfiguration.addAllowedMethod("*"); // 3允许任何方法(post、get等)
        source.registerCorsConfiguration("/**", corsConfiguration);//4处理所有请求的跨域配置
     return new CorsFilter(source);
    }
}

方式二:通过springcloud分布式里面的服务网关gateway来设置

48、springboot的事务问题, 通过@Transactional注解,这个注解一般都是写在service业务层类的方法上

场景:我们在开发企业应用时,由于数据操作在顺序执行的过程中,线上可能有各种无法预知的问题,任何一步操作都有可能发生异常,异常则会导致后续的操作无法完成。此时由于业务逻辑并未正确的完成,所以在之前操作过数据库的动作并不可靠,需要在这种情况下进行数据的回滚。

事务的作用就是为了保证用户的每一个操作都是可靠的,事务中的每一步操作都必须成功执行,只要有发生异常就回退到事务开始未进行操作的状态。这很好理解,转账、购票等等,必须整个事件流程全部执行完才能人为该事件执行成功,不能转钱转到一半,系统死了,转账人钱没了,收款人钱还没到。

事务管理是 Spring Boot框架中最为常用的功能之一,我们在实际应用开发时,基本上在service层处理业务逻辑的时候都要加上事务,当然了,有时候可能由于场景需要,也不用加事务(比如我们就要往一个表里插数据,相互没有影响,插多少是多少,不能因为某个数据挂了,把之前插的全部回滚)。

从上面的内容中可以看出,Spring Boot中使用事务非常简单,@Transactional注解即可解决问题,说是这么说,但是在实际项目中,是有很多小坑在等着我们,这些小坑是我们在写代码的时候没有注意到,而且正常情况下不容易发现这些小坑,等项目写大了,某一天突然出问题了,排查问题非常困难,到时候肯定是抓瞎,需要费很大的精力去排查问题。
1异常并没有被 ”捕获“ 到
首先要说的,就是异常并没有被 ”捕获“ 到,导致事务并没有回滚。我们在业务层代码中,也许已经考虑到了异常的存在,或者编辑器已经提示我们需要抛出异常,但是这里面有个需要注意的地方:并不是说我们把异常抛出来了,有异常了事务就会回滚,我们来看一个例子:

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;
    
    @Override
    @Transactional
    public void isertUser2(User user) throws Exception {
        // 插入用户信息
        userMapper.insertUser(user);
        // 手动抛出异常
        throw new SQLException("数据库异常");
    }
}

我们看上面这个代码,其实并没有什么问题,手动抛出一个 SQLException来模拟实际中操作数据库发生的异常,在这个方法中,既然抛出了异常,那么事务应该回滚,实际却不如此,读者可以使用我源码中 controller 的接口,通过 postman测试一下,就会发现,仍然是可以插入一条用户数据的。

那么问题出在哪呢?因为Spring Boot默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。比如上面我们的例子中抛出的RuntimeException就没有问题,但是抛出 SQLException就无法回滚了。针对非运行时异常,如果要进行事务回滚的话,可以在 @Transactional注解中使用rollbackFor属性来指定异常,比如@Transactional(rollbackFor = Exception.class),这样就没有问题了,所以在实际项目中,一定要指定异常。
2 异常被 ”吃“ 掉
这个标题很搞笑,异常怎么会被吃掉呢?还是回归到现实项目中去,我们在处理异常时,有两种方式,要么抛出去,让上一层来捕获处理;要么把异常 try catch掉,在异常出现的地方给处理掉。就因为有这中 try…catch,所以导致异常被 ”吃“ 掉,事务无法回滚。我们还是看上面那个例子,只不过简单修改一下代码:

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void isertUser3(User user) {
        try {
            // 插入用户信息
            userMapper.insertUser(user);
            // 手动抛出异常
            throw new SQLException("数据库异常");
        } catch (Exception e) {
			// 异常处理逻辑
        }
    }
}

读者可以使用我源码中controller的接口,通过 postman测试一下,就会发现,仍然是可以插入一条用户数据,说明事务并没有因为抛出异常而回滚。这个细节往往比上面那个坑更难以发现,因为我们的思维很容易导致try…catch代码的产生,一旦出现这种问题,往往排查起来比较费劲,所以我们平时在写代码时,一定要多思考,多注意这种细节,尽量避免给自己埋坑。

那这种怎么解决呢?直接往上抛,给上一层来处理即可,千万不要在事务中把异常自己 ”吃“ 掉。
3 事务的范围
事务范围这个东西比上面两个坑埋的更深!我之所以把这个也写上,是因为这是我之前在实际项目中遇到的,该场景在这个课程中我就不模拟了,我写一个 demo让大家看一下,把这个坑记住即可,以后在写代码时,遇到并发问题,就会注意这个坑了,那么这节课也就有价值了。

我来写个demo:

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public synchronized void isertUser4(User user) {
        // 实际中的具体业务……
        userMapper.insertUser(user);
    }
}


可以看到,因为要考虑并发问题,我在业务层代码的方法上加了个synchronized关键字。我举个实际的场景,比如一个数据库中,针对某个用户,只有一条记录,下一个插入动作过来,会先判断该数据库中有没有相同的用户,如果有就不插入,就更新,没有才插入,所以理论上,数据库中永远就一条同一用户信息,不会出现同一数据库中插入了两条相同用户的信息。

但是在压测时,就会出现上面的问题,数据库中确实有两条同一用户的信息,分析其原因,在于事务的范围和锁的范围问题。

从上面方法中可以看到,方法上是加了事务的,那么也就是说,在执行该方法开始时,事务启动,执行完了后,事务关闭。但是 synchronized没有起作用,其实根本原因是因为事务的范围比锁的范围大。也就是说,在加锁的那部分代码执行完之后,锁释放掉了,但是事务还没结束,此时另一个线程进来了,事务没结束的话,第二个线程进来时,数据库的状态和第一个线程刚进来是一样的。即由于mysql Innodb引擎的默认隔离级别是可重复读(在同一个事务里,SELECT的结果是事务开始时时间点的状态),线程二事务开始的时候,线程一还没提交完成,导致读取的数据还没更新。第二个线程也做了插入动作,导致了脏数据。

这个问题可以避免,第一,把事务去掉即可(不推荐);第二,在调用该 service的地方加锁,保证锁的范围比事务的范围大即可。

49、springboot整合监听器

https://blog.csdn.net/weixin_42039228/article/details/123465752

1.1 监听器介绍
web监听器是一种 Servlet 中特殊的类,它们能帮助开发者监听 web 中特定的事件,比如 ServletContext, HttpSession, ServletRequest的创建和销毁;变量的创建、销毁和修改等。可以在某些动作前后增加处理,实现监控。

监听器也叫Listener,是servlet的监听器,可以用于监听Web应用中某些对象,信息的创建,销毁,增加,修改,删除等动作的发生,然后做出相应的响应处理。当范围对象的状态发生变化时,服务器自动调用监听器对象中的方法,常用于统计在线人数和在线用户,系统加载时进行信息初始化,统计网站的访问量等。

1.2 Spring Boot中监听器的使用
web监听器的使用场景很多,比如监听servlet上下文用来初始化一些数据、监听 http session用来获取当前在线的人数、监听客户端请求的servlet request对象来获取用户的访问信息等等。这一节中,我们主要通过这三个实际的使用场景来学习一下 Spring Boot中监听器的使用。

1.2.1 监听Servlet上下文对象
监听 servlet上下文对象可以用来初始化数据,用于缓存。什么意思呢?我举一个很常见的场景,比如用户在点击某个站点的首页时,一般都会展现出首页的一些信息,而这些信息基本上或者大部分时间都保持不变的,但是这些信息都是来自数据库。如果用户的每次点击,都要从数据库中去获取数据的话,用户量少还可以接受,如果用户量非常大的话,这对数据库也是一笔很大的开销。

针对这种首页数据,大部分都不常更新的话,我们完全可以把它们缓存起来,每次用户点击的时候,我们都直接从缓存中拿,这样既可以提高首页的访问速度,又可以降低服务器的压力。如果做的更加灵活一点,可以再加个定时器,定期的来更新这个首页缓存。就类似与 CSDN个人博客首页中排名的变化一样。

下面我们针对这个功能,来写一个 demo,在实际中,读者可以完全套用该代码,来实现自己项目中的相关逻辑。首先写一个Service,模拟一下从数据库查询数据:

@Service
public class UserService {

    /**
     * 获取用户信息
     * @return
     */
    public User getUser() {
        // 实际中会根据具体的业务场景,从数据库中查询对应的信息
        return new User(1L, "倪升武", "123456");
    }
}

然后写一个监听器,实现ApplicationListener 接口,重写 onApplicationEvent方法,将ContextRefreshedEvent对象传进去。如果我们想在加载或刷新应用上下文时,也重新刷新下我们预加载的资源,就可以通过监听 ContextRefreshedEvent来做这样的事情。如下:

/**
 * 使用ApplicationListener来初始化一些数据到application域中的监听器
 */
@Component
public class MyServletContextListener implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        // 先获取到application上下文
        ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
        // 获取对应的service
        UserService userService = applicationContext.getBean(UserService.class);
        User user = userService.getUser();
        // 获取application域对象,将查到的信息放到application域中
        ServletContext application = applicationContext.getBean(ServletContext.class);
        application.setAttribute("user", user);
    }
}


正如注释中描述的一样,首先通过 contextRefreshedEvent来获取 application上下文,再通过application 上下文来获取 UserService 这个bean,项目中可以根据实际业务场景,也可以获取其他的 bean,然后再调用自己的业务代码获取相应的数据,最后存储到 application域中,这样前端在请求相应数据的时候,我们就可以直接从 application 域中获取信息,减少数据库的压力。下面写一个Controller直接从 application域中获取 user 信息来测试一下。

@RestController
@RequestMapping("/listener")
public class TestController {

    @GetMapping("/user")
    public User getUser(HttpServletRequest request) {
        ServletContext application = request.getServletContext();
        return (User) application.getAttribute("user");
    }
}


启动项目,在浏览器中输入 http://localhost:8080/listener/user测试一下即可,如果正常返回user信息,那么说明数据已经缓存成功。不过 application 这种是缓存在内存中,对内存会有消耗,后面的课程中我会讲到 redis,到时候再给大家介绍一下 redis 的缓存。

1.2.2 监听HTTP会话 Session对象
监听器还有一个比较常用的地方就是用来监听 session对象,来获取在线用户数量,现在有很多开发者都有自己的网站,监听 session 来获取当前在下用户数量是个很常见的使用场景,下面来介绍一下如何来使用。

/**
 * 使用HttpSessionListener统计在线用户数的监听器
 */
@Component
public class MyHttpSessionListener implements HttpSessionListener {

    private static final Logger logger = LoggerFactory.getLogger(MyHttpSessionListener.class);

    /**
     * 记录在线的用户数量
     */
    public Integer count = 0;

    @Override
    public synchronized void sessionCreated(HttpSessionEvent httpSessionEvent) {
        logger.info("新用户上线了");
        count++;
        httpSessionEvent.getSession().getServletContext().setAttribute("count", count);
    }

    @Override
    public synchronized void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
        logger.info("用户下线了");
        count--;
        httpSessionEvent.getSession().getServletContext().setAttribute("count", count);
    }
}


可以看出,首先该监听器需要实现HttpSessionListener接口,然后重写sessionCreated和 sessionDestroyed方法,在sessionCreated方法中传递一个HttpSessionEvent 对象,然后将当前session 中的用户数量加1,sessionDestroyed方法刚好相反,不再赘述。然后我们写一个 Controller 来测试一下。

@RestController
@RequestMapping("/listener")
public class TestController {

    /**
     * 获取当前在线人数,该方法有bug
     * @param request
     * @return
     */
    @GetMapping("/total")
    public String getTotalUser(HttpServletRequest request) {
        Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
        return "当前在线人数:" + count;
    }
}


该 Controller 中是直接获取当前session中的用户数量,启动服务器,在浏览器中输入 localhost:8080/listener/total可以看到返回的结果是1,再打开一个浏览器,请求相同的地址可以看到 count是2 ,这没有问题。但是如果关闭一个浏览器再打开,理论上应该还是2,但是实际测试却是 3。原因是 session销毁的方法没有执行(可以在后台控制台观察日志打印情况),当重新打开时,服务器找不到用户原来的 session,于是又重新创建了一个session,那怎么解决该问题呢?我们可以将上面的Controller方法改造一下:

@GetMapping("/total2")
public String getTotalUser(HttpServletRequest request, HttpServletResponse response) {
    Cookie cookie;
    try {
        // 把sessionId记录在浏览器中
        cookie = new Cookie("JSESSIONID", URLEncoder.encode(request.getSession().getId(), "utf-8"));
        cookie.setPath("/");
        //设置cookie有效期为2天,设置长一点
        cookie.setMaxAge( 48*60 * 60);
        response.addCookie(cookie);
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
    return "当前在线人数:" + count;
}


可以看出,该处理逻辑是让服务器记得原来那个 session,即把原来的sessionId记录在浏览器中,下次再打开时,把这个 sessionId传过去,这样服务器就不会重新再创建了。重启一下服务器,在浏览器中再次测试一下,即可避免上面的问题。
1.2.3 监听客户端请求Servlet Request对象
使用监听器获取用户的访问信息比较简单,实现ServletRequestListener接口即可,然后通过request对象获取一些信息。如下:

/**
 * 使用ServletRequestListener获取访问信息
 */
@Component
public class MyServletRequestListener implements ServletRequestListener {

    private static final Logger logger = LoggerFactory.getLogger(MyServletRequestListener.class);

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();
        logger.info("session id为:{}", request.getRequestedSessionId());
        logger.info("request url为:{}", request.getRequestURL());

        request.setAttribute("name", "倪升武");
    }

    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

        logger.info("request end");
        HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();
        logger.info("request域中保存的name值为:{}", request.getAttribute("name"));
    }
}


这个比较简单,不再赘述,接下来写一个 Controller 测试一下即可。

@GetMapping("/request")
public String getRequestInfo(HttpServletRequest request) {
    System.out.println("requestListener中的初始化的name数据:" + request.getAttribute("name"));
    return "success";
}


1.3 Spring Boot中自定义事件监听
在实际项目中,我们往往需要自定义一些事件和监听器来满足业务场景,比如在微服务中会有这样的场景:微服务A在处理完某个逻辑之后,需要通知微服务B去处理另一个逻辑,或者微服务 A 处理完某个逻辑之后,需要将数据同步到微服务 B,这种场景非常普遍,这个时候,我们可以自定义事件以及监听器来监听,一旦监听到微服务A 中的某事件发生,就去通知微服务 B 处理对应的逻辑。

1.自定义事件,一般是继承ApplicationEvent抽象类
2.定义事件监听器,一般是实现ApplicationListener接口
3.发布事件

1.3.1 自定义事件
自定义事件需要继承 ApplicationEvent对象,在事件中定义一个User对象来模拟数据,构造方法中将 User对象传进来初始化。如下:

/**
 * 自定义事件
 */
public class MyEvent extends ApplicationEvent {

    private User user;

    public MyEvent(Object source, User user) {
        super(source);
        this.user = user;
    }

    // 省去get、set方法
}


1.3.2 自定义监听器
接下来,自定义一个监听器来监听上面定义的MyEvent事件,自定义监听器需要实现 ApplicationListener接口即可。如下:

/**
 * 自定义监听器,监听MyEvent事件
 */
@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent myEvent) {
        // 把事件中的信息获取到
        User user = myEvent.getUser();
        // 处理事件,实际项目中可以通知别的微服务或者处理其他逻辑等等
        System.out.println("用户名:" + user.getUsername());
        System.out.println("密码:" + user.getPassword());
    }
}


然后重写onApplicationEvent方法,将自定义的MyEvent事件传进来,因为该事件中,我们定义了 User对象(该对象在实际中就是需要处理的数据,在下文来模拟),然后就可以使用该对象的信息了。

OK,定义好了事件和监听器之后,需要手动发布事件,这样监听器才能监听到,这需要根据实际业务场景来触发,针对本文的例子,我写个触发逻辑,如下:

/**
 * UserService
 */
@Service
public class UserService {

    @Resource
    private ApplicationContext applicationContext;

    /**
     * 发布事件
     * @return
     */
    public User getUser2() {
        User user = new User(1L, "倪升武", "123456");
        // 发布事件
        MyEvent event = new MyEvent(this, user);
        applicationContext.publishEvent(event);
        return user;
    }
}


在 service中注入ApplicationContext,在业务代码处理完之后,通过ApplicationContext对象手动发布 MyEvent事件,这样我们自定义的监听器就能监听到,然后处理监听器中写好的业务逻辑。

最后,在 Controller中写一个接口来测试一下:

@GetMapping("/request")
public String getRequestInfo(HttpServletRequest request) {
    System.out.println("requestListener中的初始化的name数据:" + request.getAttribute("name"));
    return "success";
}


在浏览器中输入 http://localhost:8080/listener/publish,然后观察一下控制台打印的用户名和密码,即可说明自定义监听器已经生效。

50、ThreadLocal类

https://blog.csdn.net/u010445301/article/details/111322569

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
如上文所述,ThreadLocal 适用于如下两种场景

1、每个线程需要有自己单独的实例
2、实例需要在多个方法中共享,但不希望被多线程共享
对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。

对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。

使用场景:

1)存储用户Session

一个简单的用ThreadLocal来存储Session的例子:

private static final ThreadLocal threadSession = new ThreadLocal();
 
    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }

场景二、数据库连接,处理数据库事务

场景三、数据跨层传递(controller,service, dao)

  每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。

比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。这个例子和存储session有些像。

package com.kong.threadlocal;
 
 
public class ThreadLocalDemo05 {
    public static void main(String[] args) {
        User user = new User("jack");
        new Service1().service1(user);
    }
 
}
 
class Service1 {
    public void service1(User user){
        //给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
        UserContextHolder.holder.set(user);
        new Service2().service2();
    }
}
 
class Service2 {
    public void service2(){
        User user = UserContextHolder.holder.get();
        System.out.println("service2拿到的用户:"+user.name);
        new Service3().service3();
    }
}
 
class Service3 {
    public void service3(){
        User user = UserContextHolder.holder.get();
        System.out.println("service3拿到的用户:"+user.name);
        //在整个流程执行完毕后,一定要执行remove
        UserContextHolder.holder.remove();
    }
}
 
class UserContextHolder {
    //创建ThreadLocal保存User对象
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
 
class User {
    String name;
    public User(String name){
        this.name = name;
    }
}
 
执行的结果:
 
service2拿到的用户:jack
service3拿到的用户:jack

场景四、Spring使用ThreadLocal解决线程安全问题

我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。

一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9-2所示。
img

这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。

(1):工具类里面定义ThreadLocal

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vCdT72xK-1682653820324)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1681472104348.png)]

(2):过滤器里获取id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QGSlFW7X-1682653820324)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1681472221409.png)]

3:使用这个ThreadLocal里面存储的线程值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WJqJtL8K-1682653820324)(C:\Users\coder\AppData\Roaming\Typora\typora-user-images\1681472254985.png)]

51、层次分级

一般分为:controller层, service层, mapper层, entity层, config层, dto层, filter层, interceptor层, 全局异常处理层, entity层的那些实体类的属性是和数据库的表结构一一对应的,dto层的那些类的属性是和前端要接受的数据一一对应的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值