2.ServletContainerInitalizer
容器在启动应用的时候,会扫描当前应用每一个jar包里面的src/main/webapp/META-INF/services/javax.servlet.ServletContainerInitializer指定实现的类,启动并运行这个实现类的方法。
(1).创建我们的指定扫描的文件并编辑
(2).编写被扫描的文件信息
package com.jsxs.servlet; import com.jsxs.services.HelloService; import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.annotation.HandlesTypes; import java.util.Set; /** * @Author Jsxs * @Date 2023/8/27 10:41 * @PackageName:com.jsxs.servlet * @ClassName: MyServletContainerInitalizer * @Description: TODO * @Version 1.0 */ // 容器启动的时候会将@HandlesTypes指定的这个类型下面的子类(实现类,子接口,抽象类)传递过来 @HandlesTypes(value = {HelloService.class}) // 传入我们感兴趣的类型并赋值给set 🚹 public class MyServletContainerInitializer implements ServletContainerInitializer { // 1.继承接口并实现方法 ⭐ // 2.应用启动的时候会调用这个方法 /** * @param set : 负责接受我们感兴趣类型的所有子类型 即HelloService🚹 * @param servletContext: 代表当前web应用的ServletContext,一个web应用相当于一个ServletContext * @throws ServletException */ @Override public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException { System.out.println("感兴趣的类型"+set); for (Class<?> aClass : set) { System.out.println(aClass); } } }
1.抽象类
package com.jsxs.services; /** * @Author Jsxs * @Date 2023/8/27 10:54 * @PackageName:com.jsxs.services * @ClassName: HelloServiceAbart * @Description: TODO 父接口的抽象子类 * @Version 1.0 */ public abstract class HelloServiceAbart implements HelloService{ }
2.子接口: 接口继承接口
package com.jsxs.services; /** * @Author Jsxs * @Date 2023/8/27 10:53 * @PackageName:com.jsxs.services * @ClassName: HelloServiceExt * @Description: TODO 父接口的子接口 * @Version 1.0 */ public interface HelloServiceExt extends HelloService{ }
3.实现类
package com.jsxs.services; /** * @Author Jsxs * @Date 2023/8/27 10:56 * @PackageName:com.jsxs.services * @ClassName: HelloServiceImpi * @Description: TODO 普通类继承接口 * @Version 1.0 */ public class HelloServiceImpi implements HelloService{ }
(3).将要被扫描的类全限定名写入文件中
com.jsxs.servlet.MyServletContainerInitializer • 1
(4).运行结果
我们发现不管是子类 子接口 还是抽象类 实现类都被打印出全限定名了。
3.ServletContext 注册三大组件
过滤器 + 监听器 + Servlet
1.要注册的过滤器
package com.jsxs.servlet; import javax.servlet.*; import java.io.IOException; /** * @Author Jsxs * @Date 2023/8/27 12:28 * @PackageName:com.jsxs.servlet * @ClassName: UserFilter * @Description: TODO 过滤器: 使用的是Servlet的包 * @Version 1.0 */ public class UserFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } // 过滤请求,假如为false那么就拦截,true就放行 @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { // 过滤请求 System.out.println("UserFilter..."); // 放行 filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { } }
2.要注册的监听器
package com.jsxs.servlet; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; /** * @Author Jsxs * @Date 2023/8/27 12:34 * @PackageName:com.jsxs.servlet * @ClassName: UserListener * @Description: TODO 监听项目的启动和停止 * @Version 1.0 */ public class UserListener implements ServletContextListener { // 监听项目的开始和初始化 @Override public void contextInitialized(ServletContextEvent sce) { System.out.println("UserListener---项目初始化成功...."); } // 监听销毁 @Override public void contextDestroyed(ServletContextEvent sce) { System.out.println("UserListener---项目关闭成功...."); } }
3. 要注册的Servlet
package com.jsxs.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Author Jsxs * @Date 2023/8/27 12:39 * @PackageName:com.jsxs.servlet * @ClassName: UserServlet * @Description: TODO * @Version 1.0 */ public class UserServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("tomcate..."); } }
4.进行注册的操作
package com.jsxs.servlet; import com.jsxs.services.HelloService; import javax.servlet.*; import javax.servlet.annotation.HandlesTypes; import java.util.EnumSet; import java.util.Set; /** * @Author Jsxs * @Date 2023/8/27 10:41 * @PackageName:com.jsxs.servlet * @ClassName: MyServletContainerInitalizer * @Description: TODO * @Version 1.0 */ // 容器启动的时候会将@HandlesTypes指定的这个类型下面的子类(实现类,子接口)传递过来 @HandlesTypes(value = {HelloService.class}) // 传入我们感兴趣的类型并赋值给set 🚹 public class MyServletContainerInitializer implements ServletContainerInitializer { // 1.继承接口并实现方法 ⭐ // 2.应用启动的时候会调用这个方法 /** * @param set : 负责接受我们感兴趣类型的所有子类型 即HelloService🚹 * @param servletContext: 代表当前web应用的ServletContext,一个web应用相当于一个ServletContext * @throws ServletException */ @Override public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException { System.out.println("感兴趣的类型"+set); for (Class<?> aClass : set) { System.out.println(aClass); } // 这里我们向容器中注册监听组件和过滤组件 ⭐⭐ // (1).添加Servlet组件 -> 具体的映射请求 ⭐⭐⭐ ServletRegistration.Dynamic userServlet = servletContext.addServlet("UserServlet", new UserServlet()); // 配置要映射请求的路径 userServlet.addMapping("/user"); // (2).过滤器⭐⭐⭐ FilterRegistration.Dynamic userFilter = servletContext.addFilter("UserFilter", UserFilter.class); // 配置映射的信息 userFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST),true,"/*"); // 请求方式 是否为true 映射路径 // (3).添加监听器⭐⭐⭐ servletContext.addListener(UserListener.class); } }
4.SpringMVC整合分析
1.web容器在启动的时候,会扫描每个jar包下的src/main/webapp/META-INF/services/javax.servlet.ServletContainerInitializer的全限定名 2.根据全限定名加载这个文件指定的类 3.Spring的应用一启动会加载感兴趣的WebApplicationInitializer下的所有组件 4.并且为WebApplicationInitializer组件创建对象(组件不是接口,不是抽象类) (1).AbstractContextLoaderInitializer 创建跟容器,createRootApplicationContext(); (2).AbstractDispatcherServletInitializer 1.创建一个web的ioc容器 createServletApplicationContext() 2.创建一个DispatchServlet createDispatchServlet() 3.将创建的DispatchServlet添加到ServletContext中 (3). AbstractAnnotationConfigDispatcherServletInitializer注解方式配置的DispatcherServlet初始化器 1.创建更容器: createRootApplicationContext() getRootConfigClasses(); 传入一个配置类 2.创建web的ioc容器 createServletApplicationContext() 总结: 以注解的方式启动SpringMvc,继承AbstractAnnotationConfigDispatcherServletInitializer实现抽象发发指定DipatcherServlet的配置信息
5.Servlet 异步
(1).AsyncContext 实现异步处理
有时候我们在处理一个数据的时候,会有相对应的时间限制。在实际的用户体验中可能会产生很糟糕的效应。所以我们要使用异步的方式来减少时间差。
package com.jsxs.servlet; import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Author Jsxs * @Date 2023/8/27 16:10 * @PackageName:com.jsxs.servlet * @ClassName: HelloAsyncServlet * @Description: TODO * @Version 1.0 */ @WebServlet(value = "/async",asyncSupported = true) // 设置映射的的路径和支持异步 ⭐ public class HelloAsyncServlet extends HttpServlet { @Override protected void doGet(final HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { final AsyncContext asyncContext = req.startAsync(); // 2.开启异步模式 ⭐⭐ System.out.println("主线程----------------------"); // 3.通过使用线程的方式进行异步处理 ⭐⭐⭐ asyncContext.start(new Runnable() { @Override public void run() { System.out.println("hello-------------------------------------"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } // 4. 通知异步处理完毕 ⭐⭐⭐⭐ asyncContext.complete(); // 5.获取到异步的上下文 ⭐⭐⭐⭐⭐ AsyncContext asyncContext1 = req.getAsyncContext(); // 6.获取响应 ⭐⭐⭐⭐⭐⭐ ServletResponse response = asyncContext1.getResponse(); try { response.getWriter().write("hello async..."); } catch (IOException e) { e.printStackTrace(); } } }); } }
(2).Callable 实现异步处理
package com.jsxs.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.util.concurrent.Callable; /** * @Author Jsxs * @Date 2023/8/27 16:37 * @PackageName:com.jsxs.controller * @ClassName: AsyncController * @Description: TODO * @Version 1.0 */ @Controller public class AsyncController { @RequestMapping("/async") @ResponseBody public Callable<String> async01(){ System.out.println("主线程开始"+System.currentTimeMillis()); Callable<String> callable=new Callable<String>(){ @Override public String call() throws Exception { System.out.println("副线程开始A"+System.currentTimeMillis()); Thread.sleep(2000); System.out.println("副线程结束B"+System.currentTimeMillis()); return "Callabled"; } }; System.out.println("主线程结束"+System.currentTimeMillis()); return callable; } }
(3).DeferredResult 实现异步
我们以创建订单为例,创建订单的请求一进来,应用1就要启动一个线程,来帮我们处理这个请求。如果假设应用1并不能创建订单,创建订单需要应用2来完成,那么此时应该怎么办呢?应用1可以把创建订单的消息存放在消息中间件中,比如RabbitMQ、Kafka等等,而应用2就来负责监听这些消息中间件里面的消息,一旦它发现有创建订单这个消息,那么它就进行相应处理,然后将处理完成后的结果,比如订单的订单号等等,再次存放在消息中间件中,接着应用1再启动一个线程,例如线程2,来监听消息中间件中的返回结果,只要订单创建完毕,它就会拿到返回的结果(即订单的订单号),最后将其响应给客户端。
1.创建订单
package com.meimeixia.controller; import java.util.concurrent.Callable; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.async.DeferredResult; @Controller public class AsyncController { @ResponseBody @RequestMapping("/createOrder") public DeferredResult<Object> createOrder() { /* * 在创建DeferredResult对象时,可以像下面这样传入一些参数哟! * * 第一个参数(timeout): 超时时间。限定(请求?)必须在该时间内执行完,如果超出时间限制,那么就会返回一段错误的提示信息(timeoutResult) * 第二个参数(timeoutResult):超出时间限制之后,返回的错误提示信息 */ DeferredResult<Object> deferredResult = new DeferredResult<>((long)3000, "create fail..."); ⭐ return deferredResult; } @ResponseBody @RequestMapping("/async01") public Callable<String> async01() { System.out.println("主线程开始..." + Thread.currentThread() + "==>" + System.currentTimeMillis()); Callable<String> callable = new Callable<String>() { @Override public String call() throws Exception { System.out.println("副线程开始..." + Thread.currentThread() + "==>" + System.currentTimeMillis()); Thread.sleep(2000); // 我们来睡上2秒 System.out.println("副线程结束..." + Thread.currentThread() + "==>" + System.currentTimeMillis()); // 响应给客户端一串字符串,即"Callable<String> async01()" return "Callable<String> async01()"; } }; System.out.println("主线程结束..." + Thread.currentThread() + "==>" + System.currentTimeMillis()); return callable; } }
很明显这是超出时间限制了,应该是相当于创建订单失败了吧!那接下来,我们应该怎么办呢?真令人头疼😭
OK,我们不妨来模拟一个队列。首先新建一个类,例如DeferredResultQueue,如下所示。
2.模拟队列
package com.meimeixia.service; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import org.springframework.web.context.request.async.DeferredResult; public class DeferredResultQueue { // DeferredResult对象临时保存的地方 private static Queue<DeferredResult<Object>> queue = new ConcurrentLinkedQueue<DeferredResult<Object>>(); ⭐ // 临时保存DeferredResult对象的方法 ⭐⭐ public static void save(DeferredResult<Object> deferredResult) { queue.add(deferredResult); } // 获取DeferredResult对象的方法 ⭐⭐⭐ public static DeferredResult<Object> get() { /* * poll():检索并且移除,移除的是队列头部的元素 */ return queue.poll(); } }
然后,修改一下AsyncController中的createOrder方法。上面我也已经说过了,该方法并不能真正地来处理创建订单的请求。即使如此,那也没关系,因为我们可以在该方法中先把new出来的DeferredResult对象临时保存起来。
3.将正在创建中的订单临时保存起来
package com.meimeixia.controller; import java.util.concurrent.Callable; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.async.DeferredResult; import com.meimeixia.service.DeferredResultQueue; @Controller public class AsyncController { @ResponseBody @RequestMapping("/createOrder") public DeferredResult<Object> createOrder() { /* * 在创建DeferredResult对象时,可以像下面这样传入一些参数哟! * * 第一个参数(timeout): 超时时间。限定(请求?)必须在该时间内执行完,如果超出时间限制,那么就会返回一段错误的提示信息(timeoutResult) * 第二个参数(timeoutResult):超出时间限制之后,返回的错误提示信息 */ DeferredResult<Object> deferredResult = new DeferredResult<>((long)3000, "create fail..."); DeferredResultQueue.save(deferredResult); ⭐// 保存起来 return deferredResult; } @ResponseBody @RequestMapping("/async01") public Callable<String> async01() { System.out.println("主线程开始..." + Thread.currentThread() + "==>" + System.currentTimeMillis()); Callable<String> callable = new Callable<String>() { @Override public String call() throws Exception { System.out.println("副线程开始..." + Thread.currentThread() + "==>" + System.currentTimeMillis()); Thread.sleep(2000); // 我们来睡上2秒 System.out.println("副线程结束..." + Thread.currentThread() + "==>" + System.currentTimeMillis()); // 响应给客户端一串字符串,即"Callable<String> async01()" return "Callable<String> async01()"; } }; System.out.println("主线程结束..." + Thread.currentThread() + "==>" + System.currentTimeMillis()); return callable; } }
当然了,实际开发中应该有一个别的线程来专门监听这个事,啥事?我猜应该是在其他的地方临时保存DeferredResult对象这件事吧!不过,我们在这儿并不会这样做,而是再在AsyncController中编写一个方法,例如create,该方法才是真正来处理创建订单的请求的。
4.真正创建订单号(监听)
@ResponseBody @RequestMapping("/create") public String create() { // 在这模拟创建订单 String order = UUID.randomUUID().toString(); /* * 如果我们想在上一个请求(即createOrder)中使用订单,那么该怎么办呢?从临时保存DeferredResult对象的地方获取 * 到刚才保存的DeferredResult对象,然后调用其setResult方法设置结果,例如设置订单的订单号 */ DeferredResult<Object> deferredResult = DeferredResultQueue.get(); // ⭐ deferredResult.setResult(order); // 设置结果 // 这儿给客户端直接响应"success===>订单号"这样的字符串,不要再跳转页面了 return "success===>" + order; }
至此,可以看到,只要客户端发送一个createOrder
请求进来创建订单,那么服务端就会先将new出来的DeferredResult对象临时保存起来。等到create方法触发并把订单号设置进去之后,在createOrder方法中就会立即得到返回结果,即订单号。
最后,我们来重启项目进行测试。重启成功之后,先来访问createOrder请求,以便来创建订单,但是订单必须得在3秒内创建完,所以一旦访问了createOrder请求后,你必须立即访问create请求来真正创建订单,而且至少得在3秒内完成。
这时,你会看到什么结果呢?可以看到访问create请求之后,直接给浏览器页面响应了一个success===>4e7e3e4c-27d2-4989-87c1-00d545e05feb这样的字符串,其中4e7e3e4c-27d2-4989-87c1-00d545e05feb就是所创建订单的订单号,如下图所示。
切到访问createOrder请求的浏览器窗口之后,你也可以在浏览器页面中看到所创建订单的订单号,即4e7e3e4c-27d2-4989-87c1-00d545e05feb,如下图所示。