Spring MVC框架设计及功能扩展

Posted 踩踩踩从踩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC框架设计及功能扩展相关的知识,希望对你有一定的参考价值。

目录

SpringMVC基本架构

实现一个简单的SpringMVC

控制层

方式一:实例级别的映射

 方式二:方法级别的映射

DispatcherServlet

 View

重新定义ModelAndView

 拦截处理器

SpringMVC 源码分析

 SpringMVC 安全相关

Shiro

JWT

OAuth2.0

SpringMVC请求处理流程


SpringMVC基本架构

mvc是一种软件设计典范,用一种业务逻辑、数据、界面显示分层, 分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。

Java领域典型的MVC实现及框架:

  • Servlet + jsp Servlet是曾经唯一的Java Web编程规范,现在出现了竞争者  Reactive
  • 为尽量少依赖servlet api,提高可测试性、可复用性而发展的:
  • Struts1 Struts2已成历史
  •   SpringMVc当下最流行的MVC框架,依托spring框架带来的强大开发便利性。
Struts2和SpringMVC的区别 请求映射上的区别:
  • Struts2的请求是映射到Action类级别的,每个请求创建一个Action类的实例来处理。
  • SpringMVC也支持这种方式,请求映射到实现了Controller接口的bean,但要求Controller Bean 是单例的、线程安全的。这种方式很少被采用。因为有更好的方式:请求映射到Bean的方法(方法级别的映射),这样一个类中可以处理多种请求,大大减少控制器类的数量,同时方法级更易于保证并发请求的线程安全。
请求数据的绑定上的区别:
  • Struts2将请求的数据绑定到Action类实例的属性;
  • SpringMVC则是将请求数据绑定到处理方法的参数。
专门找一个 dispatcher进行分发请求,返回不同的view 呈现 到页面上去。 MVC是严格的分工协作
  • 有四个个重要角色:DispatcherServlet Controller  View Model
  • DispatcherServlet的工作很繁重,它很重要!
  • DispatcherServlet根据什么规则来分发请求给 controller 
  • DispatcherServelt来负责转发,它又如何知道该怎么转发

实现一个简单的SpringMVC

建立一个maven工程,只需下面的依赖   
<dependency>
		    <groupId>org.springframework</groupId>
		    <artifactId>spring-context</artifactId>
		    <version>5.2.8.RELEASE</version>
		</dependency>			
		
		<dependency>
		    <groupId>javax.servlet</groupId>
		    <artifactId>javax.servlet-api</artifactId>
		    <version>4.0.1</version>
		    <scope>provided</scope>
		</dependency>

控制层

普通的javaBean来当Controller,在spring中当然希望以spring lOC容器管理的Bean来当Controller,这样可以很好地利用springloC、aop的功能! 请求如何与Bean对应,在springmvc的设计过程中 也需要考虑这个问题 两种方式 方式一:实例级别的映射 方式二∶方法级别的映射 其实类级别的映射 确实 不适合,就返回了 Struts2的 的模式 了 。

方式一:实例级别的映射

映射的规则

  • 请求地址与Bean的名称对应: Bean以请求地址为BeanNane或别名;
  • 做Controller的Bean实现Controller接口(如下),这样就知道该调用哪个方法了
才能知道 是映射到那个bean上面的属性上面 Controller处理的结果数据,以及它对应的View   返回的数据 可能是java 对象或者 view 页面。 数据多样化(未知的),被下一个处理者或view使用。

返回的结果如何存放可方便view使用  通过map 进行存储   而view是 多种多样,但是都是向浏览器做出响应的,响应的内容不同。

 最后定义出来的 类型   包括view  页面的存储 地方。 请求处理过后 就是 一个modelandview.

public interface View 
	void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response);
public interface Controller 

	ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;

 方式二:方法级别的映射

 定义的规则:

  • 普通的Bean(不需要实现Controller接口)做ontroller;
  • 请求映射到Bean的方法
加@Controller注解标识   表示一个bean是一个Controller 用RequestMapping注解  映射请求到方法

 对于请求头  参数 等的参数过滤   这个注解中可以添加上

@Documented
@Retention(RUNTIME)
@Target( TYPE, METHOD )
public @interface RequestMapping 
	/**
	 * 給定义的映射关系取个名字。如果类上、方法上都有,以#相连
	 */
	String name() default "";

	/**
	 * The primary mapping expressed by this annotation.
	 * <p>
	 * This is an alias for @link #path. For example
	 * @code @RequestMapping("/foo") is equivalent to
	 * @code @RequestMapping(path="/foo").
	 * <p>
	 * <b>Supported at the type level as well as at the method level!</b> When
	 * used at the type level, all method-level mappings inherit this primary
	 * mapping, narrowing it for a specific handler method.
	 */
	@AliasFor("path")
	String[] value() default ;

springmvc对于多种controller方式的支持

 怎么扩展分发 请求 到不同的controller上。 映射的方式也不同的 包括请求映射到实例 ,或者 映射到方法上。 并且 请求处理器handler 也是不一样的。

Controller接口实现对象

@Controller、@RequestMapping注解标识的bean的方法

不同的方式,映射规则不一样,请求处理器也不一样 ,而解决 的方式就是采用策略模式去处理

设计不同请求处理分发方式的策略接口:HandlerMapping

 将实现者配置为Bean,DispatcherServlet从ApplicationContext中获取所有配置的

在里面添加 order 接口  来排序  对于 不同的处理器上面。 以及  两种实例化的方式映射到不同的controller上去。将实现者配置为Bean,DispatcherServlet从ApplicationContext中获取所有配置的

设计不同Handler执行的适配器接口: HandlerAdapter

1、每种不同的请求处理器提供它的适配实现,配置为Bean 2 、DispatcherServlet从ApplicationContext中获取所有配置的,面向HandlerAdapter,隔绝 Handler的变化影响!

方式二的Handler是@Controller、@RequestMapping注解标识的bean和方法,需要定义一个实体 类保存BeanName,方法名,@ RequestMapping 注解信息。
public class RequestMappingInfo 

	private RequestMapping classRequestMapping;

	private String beanName;

	private Class<?> beanType;

	private RequestMapping methodRequestMapping;

	private Method method;

	private Object handler;

	public RequestMappingInfo(RequestMapping classRequestMapping, String beanName, Class<?> beanType,
			RequestMapping methodRequestMapping, Method method) 
		super();
		this.classRequestMapping = classRequestMapping;
		this.beanName = beanName;
		this.beanType = beanType;
		this.methodRequestMapping = methodRequestMapping;
		this.method = method;
	

	public boolean match(HttpServletRequest request) 

		// 1、path 不需要进行匹配了,匹配了path才选择的该Mapper

		// 2、 匹配 http Method
		if (!this.matchHttpMethod(request)) 
			return false;
		

		// 3、匹配headers
		if (!this.matchHeaders(request)) 
			return false;
		
		// 4、匹配params
		if (!this.matchParams(request)) 
			return false;
		
		return true;
	

 注解信息的获取,RequestMappingHandlerMapping,在它的实例创建好,初始化后

把注解信息进行加载注解信息。

这里就是生命周期的时候可以进行 读取注解信息  解析开来进行读取。

而这些 类的扫描信息 是asm 字节码 进行获取到原数据的。

我们此时通过ApplicationContext获取Bean的MVC注解信息,是通过原来的 AnnotatedBeanDefinition,还是加载类加载类,通过反射方式获取 MVC注解检测的逻辑
@Override
public void afterPropertiesSet() throws Exception 
// 检测@Controller Bean
for (String beanName : this.applicationContext.getBeanNamesForType(Object.class)) 
    Class<?> beanType = this.applicationContext.getType(beanName);
   if (isHandlerBean(beanType)) 
   detectHandlerMethod(beanType);
   
  

这些可以 在  handlermapping  进行处理的  检测并缓存起来的。

并且 要考虑在getHandler(request)阶段能更快速地获取

DispatcherServlet

DispatcherServlet要完成的事情
  • 1、创建ApplicationContext容器
  • 要从容器中获得HandlerMapping、 HandlerAdapter
  • 完成分发
  • 完成view转发
  • 完成异常处理

对于DispatcherServlet要完成这些事情

  • 在init方法中完成3个属性的初始化
  • 在service方法中完成handler分发、执行的逻辑
  • 在destroy方法中关闭applicationContext

 

public class DispatcherServlet extends HttpServlet 
	/**
	 * 
	 */
	private static final long serialVersionUID = -560428787709797085L;

	private static final String CONTEXT_CONFIG_LOCATION_PARAM_NAME = "contextConfigLocation";

	private static final String ROOT_APPLICATION_CONTEXT_ATTRIBUTE_NAME = "root_application_context";
	private static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE_NAME = "web_application_context";

	protected final Log logger = LogFactory.getLog(getClass());

	private GenericXmlApplicationContext webApplicationContext;

	private List<HandlerMapping> handlerMappings;

	private List<HandlerAdapter> handlerAdapters;

	public void init() throws ServletException 
		String contextConfigLocation = this.getInitParameter(CONTEXT_CONFIG_LOCATION_PARAM_NAME);
		if (StringUtils.isEmpty(contextConfigLocation)) 
			contextConfigLocation = this.getServletContext().getInitParameter(CONTEXT_CONFIG_LOCATION_PARAM_NAME);
		

		if (StringUtils.isEmpty(contextConfigLocation)) 
			throw new ServletException("没有 spring mvc指定[" + CONTEXT_CONFIG_LOCATION_PARAM_NAME + "]参数");
		

		this.webApplicationContext = new GenericXmlApplicationContext();
		// 设置它的父容器,如果有放在ServletContext中root容器
		this.webApplicationContext.setParent(
				(ApplicationContext) this.getServletContext().getAttribute(ROOT_APPLICATION_CONTEXT_ATTRIBUTE_NAME));
		// 加载xml Bean定义配置
		this.webApplicationContext.load(contextConfigLocation);
		// 刷新
		this.webApplicationContext.refresh();

		// 将webApplicationContext也放入ServletContext中
		this.getServletContext().setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE_NAME, webApplicationContext);

		// 初始化 MVC相关组件
		initStrategies(webApplicationContext);
	

	/**
	 * 初始化 MVC相关组件的策略提供者,从applicationContext中获取
	 * 
	 * @param applicationContext
	 */
	private void initStrategies(ApplicationContext applicationContext) 
		// 1、initHandlerMapping
		initHandlerMappings(applicationContext);

		// 2、initHandlerAdapter
		initHandlerAdapters(applicationContext);

	

	private void initHandlerAdapters(ApplicationContext applicationContext) 
		// 查找applicationContext中所有的HandlerMapping类型的Bean
		Map<String, HandlerAdapter> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext,
				HandlerAdapter.class, true, false);
		if (!matchingBeans.isEmpty()) 
			this.handlerAdapters = new ArrayList<>(matchingBeans.values());
		

		if (CollectionUtils.isEmpty(this.handlerAdapters)) 
			// 初始化为默认的
			SimpleControllerHandlerAdapter df = new SimpleControllerHandlerAdapter();
			this.handlerAdapters = Collections.singletonList(df);
		

	

	private void initHandlerMappings(ApplicationContext applicationContext) 
		// 查找applicationContext中所有的HandlerMapping类型的Bean
		Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext,
				HandlerMapping.class, true, false);
		if (!matchingBeans.isEmpty()) 
			this.handlerMappings = new ArrayList<>(matchingBeans.values());
			// We keep HandlerMappings in sorted order.
			AnnotationAwareOrderComparator.sort(this.handlerMappings);
		

		if (CollectionUtils.isEmpty(this.handlerMappings)) 
			// 初始化为默认的
			BeanNameUrlHandlerMapping df = new BeanNameUrlHandlerMapping();
			df.setApplicationContext(applicationContext);
			this.handlerMappings = Collections.singletonList(df);
		

	

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
		// 在这里可把一些Dispatcher持有的对象放入到Request中,以被后续处理过程中可能需要使用到
		req.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE_NAME, webApplicationContext);

		this.doDispatch(req, resp);

	

	private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
		Object handler = null;
		ModelAndView mv = null;
		Exception dispatchException = null;
		try 
			// 1、获取请求对应的handler
			handler = this.getHandler(req);
			// 2、如果没有对应的handler
			if (handler == null) 
				noHandlerFound(req, resp);
				return;
			
			// 3、如果有对应的handler,获得handler的Adapter

			HandlerAdapter ha = this.getHandlerAdapter(handler);

			// 4、执行adapter
			mv = ha.handle(req, resp, handler);

		 catch (Exception e) 
			dispatchException = e;
		
		// 5、转发给view
		processDispatchResult(req, resp, handler, mv, dispatchException);
	

	private void processDispatchResult(HttpServletRequest req, HttpServletResponse resp, Object handler,
			ModelAndView mv, Exception dispatchException) throws ServletException 
		// 1、 如果有异常,生成对应的异常视图
		if (dispatchException != null) 
			mv = doProcessException(req, resp, handler, dispatchException);
		
		// 2、渲染视图
		render(req, resp, mv);

	

	private ModelAndView doProcessException(HttpServletRequest req, HttpServletResponse resp, Object handler,
			Exception dispatchException) 
		// TODO Auto-generated method stub
		return null;
	

	private void render(HttpServletRequest req, HttpServletResponse resp, ModelAndView mv) throws ServletException 
		// 1、判断MV的view是否是一个viewName,如是转为对应的view
		View view;
		String viewName = mv.getViewName();
		if (viewName != null) 
			// 将viewName转为对应的view.
			view = resolveViewName(viewName, mv.getModel(), req);
			if (view == null) 
				throw new ServletException("Could not resolve view with name '" + mv.getViewName()
						+ "' in servlet with name '" + getServletName() + "'");
			
		 else 
			// 是一个真正的view对象
			view = mv.getView();
			if (view == null) 
				throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a "
						+ "View object in servlet with name '" + getServletName() + "'");
			
		

		try 
			if (mv.getStatus() != null) 
				resp.setStatus(mv.getStatus().value());
			
			view.render(mv.getModel(), req, resp);
		 catch (Exception ex) 
			if (logger.isDebugEnabled()) 
				logger.debug("Error rendering view [" + view + "]", ex);
			
			throw ex;
		
	

	private View resolveViewName(String viewName, Map<String, Object> model, HttpServletRequest req) 
		// TODO Auto-generated method stub
		return null;
	

	private HandlerAdapter getHandlerAdapter(Object handler) 
		for (HandlerAdapter ha : this.handlerAdapters) 
			if (ha.supports(handler)) 
				return ha;
			
		
		return null;
	

	private void noHandlerFound(HttpServletRequest req, HttpServletResponse response) throws IOException 
		// TODO 如果没有特定的处理规则,就直接404了
		response.sendError(HttpServletResponse.SC_NOT_FOUND);

	

	private Object getHandler(HttpServletRequest req) throws Exception 
		Object handler = null;
		for (HandlerMapping hm : this.handlerMappings) 
			handler = hm.getHandler(req);
			if (handler != null) 
				return handler;
			
		
		return null;
	

	@Override
	public void destroy() 
		this.webApplicationContext.close();
	

 View

要返回ModelAndView,方法中就需要创建它的实例,需提供view对象,这样Controller

 做到灵活的扩展,自动映射的情况。

解决嵌入式的问题。

重新定义ModelAndView

添加视图的名字。加入视图名称,由HandlerAdapter根据handler返回结果来创建

ModelAndView;  需要一个专门处理视图的处理层。 View-ViewResolver ViewResolver完成从视图名到视图的转换。 不同的视图技术可能有不同的View实现及转换规则,那就实现ViewResolver来提供 对应的转换规则,都配置为Bean,DispatcherServlet好从容器中获取!

 

 这里面要做的 包括 对应前端页面的  怎么判断是jsp html  对应的资源,或者 转发   重定向 等等。

这个处理层 一定需要做的。

这里的解决办法 就采用 字符串带前缀:forward:redirect: 来定义第一个实现!

默认就是字符串 其他方式就采用添加前缀来表示。

一个基于URL的转发、重定向的ViewResolver实现

增加其他的View实现

为了方便 返回数据设计Model 

 

而不使用map来存储的并返回也是因为:

Map是一个通用的集合类型,没法在方法参数上区分是否用来返回值的。

 拦截处理器

对于请求参数 怎么绑定到handler上面去

并且对于请求进行过滤等等。

 利用Handlerlnterceptor 来处理这些事情。

HandlerInterceptor接口定义

 用户实现它来提供拦截器,可同时实现Ordered接口来指定顺序

  • 用户实现它来提供拦截器,可同时实现Ordered接口或@order来指定顺序
  • 在其上加@requestMapping来指定它要过滤的请求
  •  配置为Bean
  • HandleMapping初始化阶段完成HandlerInterceptor的加载
  • 在获取Handler阶段,把拦截器加入,形成链返回
  • 执行链

这里都是要经过过滤器进行过滤的。

SpringMVC 源码分析

servlet

se'rvlet 是运行在 Web 服务器或应用服务器上的程序,处理Http请求。Servlet规范目前已经到了4.0JavaEE8规范,不过3.0后的版本是一个分水岭。  

servlet是一个规范,然后在java中的实现。

SpringMVCServlet的关系   而springmvc 则是 在servlet协议下的一个具体实现, 在spring框架下面的一个具体实现。 TomcatServlet的关系

tomcat是web容器,而用来存储 servlet的具体实现。

通信框架与TomcatServlet

 而通信框架 则是属于交互部分

 Servlet4.0特性

HTTP/2 做准备,全包含服务器推送 HTTPS 3.1 中的注解和异步是其中非常抢眼的功能。 添加下面的配置  这是类似web.xml的配置方式 采用java代码中来实现出来的。 这个接口也提供 了对于功能的提升。
@ComponentScan(value="courseware.springmvc.mvc",includeFilters=
		@Filter(type=FilterType.ANNOTATION,classes=Controller.class)
,useDefaultFilters=false)
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer 
	
	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) 
		//默认所有的页面都从 /WEB-INF/ xxx .jsp
		//registry.jsp();
		registry.jsp("/WEB-INF/", ".jsp");
	
	
	//静态资源访问
	@Override
	public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) 
		configurer.enable();
	
	
	@Override
	public void configureMessageConverters(List<HttpMessageConverter<?>> converters) 
		WebMvcConfigurer.super.configureMessageConverters(converters);
		// 解决乱码问题
		StringHttpMessageConverter shc = new StringHttpMessageConverter();
		List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
		supportedMediaTypes.add(MediaType.TEXT_PLAIN);
		supportedMediaTypes.add(MediaType.APPLICATION_JSON);
		supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
		shc.setSupportedMediaTypes(supportedMediaTypes);
		shc.setDefaultCharset(Charset.forName("UTF-8"));
		converters.add(shc);
		
		// @ResponseBody 转json
		Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
                .indentOutput(true)
                .dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
                .modulesToInstall(new ParameterNamesModule());
        converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
        converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
	
	
	//拦截器
	@Override
	public void addInterceptors(InterceptorRegistry registry) 
		// 测试的拦截器
		registry.addInterceptor(new MyFirstInterceptor()).addPathPatterns("/**");
		
		// JWT 令牌拦截器配置,
		registry.addInterceptor(new JwtTokenInterceptor())
		// /register 和 /login 不需要拦截
		.excludePathPatterns("/register", "/login")
		.addPathPatterns("/**");
	

servlet 4.0 新特性push 

@WebServlet(value = "/pushFile")
public class PushServlet extends HttpServlet 
	/** */
	private static final long serialVersionUID = 1L;

	/*
	 * 服务器推送使服务器能预测客户端请求的资源需求。然后,在完成请求处理之前,它可以将这些资源发送到客户端。
	 * 
	 */
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
		PushBuilder pushBuilder = req.newPushBuilder();
		// 在某些情况下,客户端可能会为请求事务拒绝服务器推送。
		// 如果客户端没有使用安全连接,服务器推送也不会起作用。
		// 因此,务必要在对 `PushBuilder` 实例调用方法之前,针对 `null` 返回值进行测试。
	 	if (pushBuilder != null) 
	 		// 一次请求,主动推送更多的资源
	    	pushBuilder.path("100x133.jpg").push();
	    	pushBuilder.path("100x133.jpg").push();
		
	 	
	 	 getServletContext()
         .getRequestDispatcher("/index.jsp")
         .forward(req, resp); 
	

针对springmvc中异步的场景,支持异步处理asyncSupported=true  异步响应。

@WebServlet(value = "/asyncServlet", asyncSupported = true)
public class AsyncServlet extends HttpServlet 
    /** */
	private static final long serialVersionUID = 1L;

	@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
        //1、支持异步处理asyncSupported=true
        //2、开启异步模式
        System.out.println("主线程开始。。。"+Thread.currentThread()+"==>"+System.currentTimeMillis());
        AsyncContext startAsync = req.startAsync();
        //3、业务逻辑进行异步处理;开始异步处理
        startAsync.start(()-> 
            try 
                System.out.println("副线程开始。。。"+Thread.currentThread()+"==>"+System.currentTimeMillis());
                sayHello();
                //获取到异步上下文
                AsyncContext asyncContext = req.getAsyncContext();
                //4、获取响应
                ServletResponse response = asyncContext.getResponse();
                response.getWriter().write("hello async...");
                System.out.println("副线程结束。。。"+Thread.currentThread()+"==>"+System.currentTimeMillis());
                // 结束异步执行
                startAsync.complete();
             catch (Exception e) 
            	e.printStackTrace();
            
        );
        System.out.println("主线程结束。。。"+Thread.currentThread()+"==>"+System.currentTimeMillis());
    

    public void sayHello() throws Exception
        System.out.println(Thread.currentThread()+" processing...");
        Thread.sleep(3000);
    

 SpringMVC 安全相关

Shiro

认证基于什么来做的? 用户信息 在服务端的 Session ,用户会话 , session   坏处就是大量占用服务器资源 并且 横向扩展的上 问题  一旦有部分走不通就会出现问题。

JWT

JSON Web Token ,双方之间传递安全信息的简洁的、 URL 安全的表述性声明规范。 需要jwt的原因 1. 鉴权逻辑无需访问数据库,任何情况下都不会击穿缓存打到数据库影响业务; 2. 与数据库解耦,横向扩容性佳, Token 发放、验证都可以脱离数据库 3. 同样是提供 Token ,但 JWT 中可附带允许的权限 / 操作,业务无需实现复杂的权限控制逻辑,只需要 判断Token 中是否有对应权限即可; 4. 数据平台主要的功能就是提供数据访问接口与数据上传接口,没有传统 Web 应用的那种上下文关 系。过重的鉴权逻辑不太符合当前业务特点; 5. 使用多版本私钥,通过更改固定版本签名所用的私钥可以批量废除发出的 Token ;使用 Redis 等缓存后亦可实现对单一Token 的回收 6. 业务只有上传 / 拉取这两个操作,只需要区分当前用户是谁即可。 什么情况下使用 JWT 比较合适 1. 只需要区分用户 2. 没有上下文的环境(有其实也可以,但这样他的优势就没那么大) 3. 不想让鉴权用到的数据库成为可能的瓶颈
public class JwtTokenInterceptor implements HandlerInterceptor 
	
	public void afterCompletion(HttpServletRequest request,
            HttpServletResponse response, Object handler, Exception arg3)
            throws Exception 
    

    public void postHandle(HttpServletRequest request, HttpServletResponse response,
            Object handler, ModelAndView model) throws Exception 
    

    // 拦截每个请求,验证token是否正常
    // 不需要在session中存储用户信息,而是通过计算验证信息
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
            Object handler) throws Exception 
        response.setCharacterEncoding("utf-8");
        String token = request.getParameter("token");
        ResponseData responseData = ResponseData.ok();
        //token不存在
        if(null != token) 
            Login login = JWT.unsign(token, Login.class);
            String loginId = request.getParameter("loginId");
            //解密token后的loginId与用户传来的loginId不一致,一般都是token过期
            if(null != loginId && null != login) 
                if(loginId.equals(login.getId())) 
                    return true;
                 else 
                    responseData = ResponseData.forbidden();
                    responseMessage(response, response.getWriter(), responseData);
                    return false;
                
             else 
                responseData = ResponseData.forbidden();
                responseMessage(response, response.getWriter(), responseData);
                return false;
            
         else 
            responseData = ResponseData.forbidden();
            responseMessage(response, response.getWriter(), responseData);
            return false;
        
    

    //请求不通过,返回错误信息给客户端
    private void responseMessage(HttpServletResponse response, PrintWriter out, ResponseData responseData) 
        responseData = ResponseData.forbidden();
        response.setContentType("application/json; charset=utf-8");
        String json = JSONObject.toJSONString(responseData);
        out.print(json);
        out.flush();
        out.close();
    

OAuth2.0

OAuth (开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。 应用场景 1. 第三方应用授权登录在APP 或者网页接入一些第三方应用时,经常会需要用户登录另一个合作平台。比如 QQ ,微博,微信的授权登录。 2. 原生 app 授权app登录请求后台接口,为了安全认证,所有请求都带 token 信息。如:登录验证、请求后台数据。 3. 前后端分离单页面应用前后端分离框架,前端请求后台数据,需要进行oauth2 安全认证。比如使用 vue react 后者 h5 开发的app

 这里介绍的主要就是授权。

授权模式
  • 授权码模式(authorization code
  • 简化模式(implicit
  • 密码模式(resource owner password credentials
  • 客户端模式(client credentials

需要手动添加 oauth 的jar包

  <!-- 不是starter,手动配置 -->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.2.RELEASE</version>
        </dependency>

针对oauth需要添加配置

/**
 * 配置资源服务器和授权服务器
 */
@Configuration
public class OAuth2ServerConfig 

    private static final String DEMO_RESOURCE_ID = "order";

    /**
     * 资源服务器。
     * 将订单作为保护资源。
     *
     */
    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter 

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) 
            resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
        

        @Override
        public void configure(HttpSecurity http) throws Exception 
            http
                .authorizeRequests()
                    .antMatchers("/order/**").authenticated();//配置order访问控制,必须认证过后才可以访问

        
    

    /**
     * 授权服务器
     *
     */
    @Configuration
    @EnableAuthorizationServer
    protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter 

        @Autowired
        AuthenticationManager authenticationManager;
        @Autowired
        RedisConnectionFactory redisConnectionFactory;


        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception 

        	// password 方案一:明文存储,用于测试,不能用于生产
        	// String finalSecret = "123456";
        	
        	// password 方案二:用 BCrypt 对密码编码
        	// String finalSecret = new BCryptPasswordEncoder().encode("123456");
        	
            // password 方案三:支持多种编码,通过密码的前缀区分编码方式
            String finalSecret = "bcrypt"+new BCryptPasswordEncoder().encode("123456");
            //配置两个客户端,一个用于client认证,一个用于password认证
            clients.inMemory().
            		/* 
            		 * client模式:没有用户的概念,直接与认证服务器交互,
            		 * 用配置中的客户端信息去申请accessToken,客户端有自己的client_id,
            		 * client_secret对应于用户的username,password,而客户端也拥有自己的authorities,
            		 * 当采取client模式认证时,对应的权限也就是客户端自己的authorities。
            		 * 
            		 * 适用场景:仅仅是接口的对接,不考虑用户
            		 * */
            		withClient("client_1")
                    .resourceIds(DEMO_RESOURCE_ID)
                    .authorizedGrantTypes("client_credentials", "refresh_token")
                    .scopes("select")
                    .authorities("oauth2")
                    .secret(finalSecret)
                    .and().
                    
                    /* 
                     * password模式:系统必须有一套用户体系,在认证时需要带上自己的用户名和密码,以及客户端的client_id,client_secret。
                     * 此时,accessToken所包含的权限是用户本身的权限,而不是客户端的权限。
                     * 
                     * 适用场景:需要考虑用户,并且有一套用户权限体系,可以采用password模式
                     * */
                    withClient("client_2")
                    .resourceIds(DEMO_RESOURCE_ID)
                    .authorizedGrantTypes("password", "refresh_token")
                    .scopes("select")
                    .authorities("oauth2")
                    .secret(finalSecret);
        

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) 
        	// 授权信息放到redis中
            endpoints
                    .tokenStore(new RedisTokenStore(redisConnectionFactory))
                    .authenticationManager(authenticationManager)
                    .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
        

        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) 
            //允许表单认证
            oauthServer.allowFormAuthenticationForClients();
        
    


这里注意需要授权 的匹配。

 

SpringMVC请求处理流程

处理的第一步建立Map<urls,controller>的关系

ApplicationObjectSupport.setApplicationContext AbstractDetectingUrlHandlerMapping.initApplicationContext(context)

 这里面有个比较重要的方法 initapplicationcontext 方法

 

 这里面进去到 handlermapping方法    加载 handler。

 

根据访问url找到对应controller中处理请求的方法  

DispatcherServlet doService doDispatch 做分发处理等,数据解析等操作。

 

反射调用处理请求的方法 , 返回结果视图 AnnotationMethodHandlerAdapter.handle

 在 springmvc中这些源码更多的就是 对 请求的处理,相对ioc aop  等简单很多  少了很多扩展点等。

以上是关于Spring MVC框架设计及功能扩展的主要内容,如果未能解决你的问题,请参考以下文章

一文学会 Spring MVC 常用注解

Spring企业级程序设计 • 第7章 Spring框架整合

Spring企业级程序设计 • 第7章 Spring框架整合

你会这些 Spring MVC 常用注解吗?

面试:给我说一下Spring MVC拦截器的原理?

spring mvc 如何配置最简洁的