为什么做 java 的 web 开发我们会使用 struts2,springMVC 和 spring 这样的框架?
原链接:http://blog.pcwuyu.com/2016/661.html
原分类:java
RubyLouvre
今年我一直在思考 web 开发里的前后端分离的问题,到了现在也颇有点心得了,随着这个问题的深入,再加以现在公司很多 web 项目的控制层的技术框架由 struts2 迁移到 springMVC,我突然有了一个新的疑问无法得到正确的解释,为什么我们现在做 java 的 web 开发,会选择 struts2 或者 springMVC 这样的框架,而不是使用 servlet 加 jsp 这样的技术呢?特别是现在我们 web 的前端页面都是使用 velocity 这样的模板语言进行开发,抛弃了 jsp,这样的选择又会给我们 java 的 web 开发带来什么样的好处,延着这个问题的思路,我又发现新的疑问,为什么现在很多 java 企业级开发都会去选择 spring 框架,spring 框架给我们开发的应用带来了什么?这么一想还真是问倒我了,我似乎很难找到一串能让人完全信服的答案,最终我发现,这些我认为我很熟悉很常用的技术,其实还有很多让我陌生不解的地方,这些陌生和不解的地方也正是我是否能更高层次使用它们的关键,今天这篇文章我就来讲讲这些问题,不过 struts2,spring 这样的技术经过这么多年的积累已经是相当庞大和复杂,它们的面很广,本人虽然已经用了它们多年,还是有很多技术不熟悉和不清楚,所以本文不是全面对我题目做出解答的文章,而是根据我现有知识结构来理解这个问题。
软件里有很多优秀的框架,有一种类型的框架,它的特点是建立在一个现有技术的基础上,提供和现有技术一样业务功能的技术框架,这个新的技术框架比原技术更加易用,更加健壮同时功能更加强大,例如:jQuery,以及本文所要谈到的 struts2 和 springMVC,深究这些框架都是相当之复杂,但是它们的优点其实只有一个:就是让使用者只关心核心业务的开发,框架帮你屏蔽原有技术跟业务开发无关的各类技术问题。像 jQuery,struts2 或 springMVC 这类框架之所以优秀,就是它们在这点上做的太好了,以至于很多使用它的程序员都已经不清楚原有技术的真实面目,因此我们要将 struts2 理解的更好,使用的更加熟练和深入,这里我们就要跳出 struts2 的技术,到 struts2 技术的源头 servlet,仔细研究下 servlet 的特点,只有这样我们才能把 struts2 框架学的更好。
Servlet 的作用是接收浏览器传给服务端的请求(request),并将服务端处理完的响应(response)返回给用户的浏览器,浏览器和服务端之间通过 http 协议进行沟通,其过程是浏览器根据用户的选择将相关信息按 http 协议报文的规范组装请求的 http 报文,报文通过网络传输到指定的服务器,服务器通过特定的 web 容器接收这个报文信息,例如:tomcat,jetty,jboss 这样的 web 容器,web 容器会将 http 报文解析出来,如果是用户请求,最终解析出来的报文信息会用一个 request 对象存储起来,服务端使用这个 request 做完相应的处理后,服务端程序将结果信息封装到 response 对象里,然后将 response 对象交给 web 容器,web 容器则把这个 response 对象转变为 http 协议的报文,并将报文回传给浏览器,浏览器最后解析这个响应报文,将最终结果展示给用户。
Web 容器创造了 servlet 接口,servlet 接口就是开发人员自己实现业务逻辑的地方,程序员开发 servlet 就好比做填空题,而填空题的语境或者说上下文提示就是由 request 和 response 对象,但是 javaEE 规范里的 servlet 接口很简单,就三个方法 init,service 和 destory,但是这个接口太笼统,所以规范里还提供了一个 HttpServlet 类,这个类根据 http 请求类型提供了 doGet,doPost 等方法,servlet 接口最大的特点就是根据 http 协议的特点进行定义,因此做 servlet 开发时候如果使用者对 http 协议特点不是特别熟悉,都会碰到或多或少令人迷惑的问题,特别是碰到一些复杂特殊的请求时候:例如文件上传,返回特殊的文件格式到浏览器,这时候使用 servlet 开发就不是很方便了,servlet 开发还有个问题可能大家常常被忽视,就是请求的数据的类型转化,http 协议传输都是文本形式,到了 web 容器解析后也是文本类型,如果碰到货币,数字,日期这样的类型需要我们根据实际情况进行转化,如果页面传送的信息非常多,我们就不得不做大量类型转化,这种工作没有什么技术含量,是个体力活而且很容易导致程序错误。同时 java 的企业开发都是围绕 javabean 进行,类型转化好的数据还要封装到对应的 javabean 里,这种转来转去的事情对于项目开发绝对不是什么好事情,所以古老的 struts1 为这种问题找到了一种解决方案,就是定义了一个 DTO 对象(数据传输对象),专门负责做这样的事情,不过到了 struts2,整个替代 servlet 的 action 本身就是一个 javabean。
Java 的企业开发一个技术特点就是使用 javabean 进行的,struts2 的特点之一就是它替代 servlet 的操作类就是一个典型的 javabean,首先 struts2 框架将页面传输的数据进行类型转化和封装后将请求信息封装到了这个 javabean 的属性里,这样我们开发 web 程序时候就省去了烦心的类型转化和封装的问题,前面我讲到传统的 servlet 是根据 http 协议进行定义的,它会按你请求方式(post 还是 get 方式)来处理用户的请求,但是对于一名程序开发人员而言,一个请求,具体到一个 url,其实对于服务端而言就是服务端对外提供的一个功能,或者说是服务端对外的一个动作,如果我们使用 servlet 开发程序我们就得把 http 的动作转化为具体的业务动作,这就让程序开发变得繁琐,增强了开发的难度,所以 struts2 替代 servlet 的 javabean 就屏蔽了 servlet 里 http 的请求方式和具体业务动作转化的问题,javabean 里的每一个方法都可以和每一个 url 请求一一对应,这必然减轻了开发的难度问题。
Servlet 另一个作用就是构造 response 对象,让页面获得正确的响应,其实现代的浏览器是一个多媒体工具,文字,图片,视屏等等东西都可以在浏览器里显示,资源的不同就会导致 http 响应报文的差别,如果我们使用 servlet 开发就要根据资源的不同在 java 程序里用硬编码的形式处理,这样的程序很难复用,而且如果程序员对某种资源的处理理解不到位,就会导致问题的出现,struts2 通过配置文件的形式将这样的逻辑从 java 程序里剥离出来,使用配置的方式进行统一管理,这个做法和 spring 的 AOP 方式类似,这样就让结果处理方式更加统一,更加利于管理,同时也提升了程序的健壮性以及降低了开发的难度。
Servlet 在 MVC 开发模式里就是其中 C 层即控制层,控制层就像俄罗斯的双头鹰(一个头向东看一个头向西看)一样,一个头向 M 层模型层看,一个头向 V 层视图层看,模型层也是用 java 编写的,控制层也属于服务端语言开发,所以 M 层和 C 层的沟通没有天然的障碍,但是和 V 层视图层就不一样了,这是一个跨语言的沟通,对于浏览器,它只懂得 html,javascript 和 css,浏览器是理解不了 java 这种语言的东西,但是要让服务端的东西能被浏览器理解接受,我们就必须得把服务端的响应信息放到页面里,因此就需要一个技术把 java 的信息转化到 html 页面里,这就是 javaEE 规范里提供了 jsp 技术,jsp 其实是一种服务端技术而非客户端技术,不过它看起来似乎更像 html 技术,最早的 jsp 开发里都是直接将 java 代码写到页面里,这种坏处谁都知道,之后 javaEE 规范提供了自定义标签技术,使用一种类似 html 标签的方式来解析 java 代码,struts2 框架提供了一整套完整的自定义标签技术,这似乎听起来不算啥,但是它的作用非凡,因为自定义标签之所以叫自定义就是每个人都可以自己来定义,如果没有一个规范必然产生混乱,而且一套完善的自定义标签是个系统工程,一套完整的自定义标签相当于我们在自己定义一套新的开发语言,做程序的人听到这个一定就会明白开发一套完整的自定义标签的工作量和开发难度都是难以想象的,而且自定义标签都是和控制层紧密相连,其难度又会增加一个维度,所以 struts2 提供的自定义标签对于业务开发带来的将是质的飞越。
Servlet 里还有两个重要的技术:监听器和过滤器,对于监听器在 web 开发里使用的场景比较少,都是一些十分特别的情况才会使用,大部分 web 开发里可以忽略它的使用,我们用的最多的监听器可能就是对 ServletContext 创建和销毁的监听器,ServletContext 是整个 web 应用的全局对象,它和 Web 应用的生命周期绑定在一起,因此使用这个监听器对 Web 应用的全局信息进行初始化和销毁操作,例如 spring 容器的初始化操作。比较有意思的是过滤器,在 struts2 里有个拦截器,它们的作用相同都是用来拦截请求的,因为拦截器是 struts2 的特有功能,在 struts2 里使用拦截器自然比使用过滤器更顺手,其实拦截器所用的技术比过滤器更加先进,因为拦截器使用了反射技术,因此拦截器拦截的面更大,控制请求的能力更强,它能完成的任务也会更加的丰富多彩。
在我第一次接触 struts2 时候,有人告诉我 struts 设计的一个目的就是想屏蔽在控制层里操作 request 和 response 对象,因为这两个 http 协议的儿子会造成 web 开发里思路的混乱,但是我在实际开发里却经常不自觉的使用这两个对象。而且本人做前端开发非常喜欢使用 ajax,使用 ajax 技术时候我就很讨厌 struts2 的自定义标签,我更加喜欢在页面里用 javascript 技术处理各种信息,最终 struts2 在我眼里就是一个 servlet 的变体,因此曾经有段时间我常常在想是不是可以抛弃 struts2,直接用 servlet,因为 struts2 里用到了太多反射机制,特别是使用注解做配置(注解是用反射实现的),在 java 里反射的执行效率是非常低的,直接使用 servlet 一定能提升 web 应用的执行效率。其实这个倒很难做到,因为当时我没法在 servlet 里灵活的运用 spring 技术。
下面我要谈谈 spring 了。
spring 技术可以说是 java 企业开发里最重要的技术,不过真的理解 spring 的作用和意义还真是一件麻烦的事情,很多人对 spring 理解其实都是停留在使用阶段(例如:声明式事务很好用等等),当今的 spring 技术生态环境里可谓是蔚为壮观,spring 已经包罗万象,它的内容之多完全不亚于它的本源 java 语言了,而 spring 这么大的框都是建立在 ioc 和 aop 技术之上,只有深入理解了这两个技术我们才能明白为什么 spring 这个框能装的下那么多东西了。
首先是 ioc,ioc 技术第一个解释叫做控制反转,它还有个解释就是依赖注入,这两个名字很难从字面理解,但是当你理解它的原理后就会发现它们的描述是何等准确。Ioc 技术的本质就是构建对象的技术换句话说就是将一个类实例化成对象的技术,在 java 里实例化类通过 new 关键字进行的,每次 new 一个类都会产生一个新的实例对象,这么做视乎很浪费,有时这种浪费还挺危险,因为在程序开发时候我们常常只需要某个类永远只能产生一个的实例对象这个时候就得使用单例模式,此外在设计模式里还可以通过工厂方式产生对象,使用过 spring 的人看到上面的文字就知道了,spring 里 bean 的定义就和上面的内容一一对应,scope 属性 single 产生单例对象,prototype 产生新对象,bean 还可以通过工厂方式产生对象,可以说 spring 的 bean 就是制造对象的工具。面向对象编程里对象相当于显示生活中的一个实体,例如我们有个对象作用是完成打猎的操作,那么打猎这个对象内部包含两个辅助对象:人和枪,只有人和枪赋予了打猎这个对象,那么打猎对象才能完成打猎的操作,但是构建一个人和枪的对象并不是看起来那么简单,这里以枪为例,要创造一把枪我们需要金属,需要机床,需要子弹,而机床和子弹又是两个新对象,这些对象一个个相互嵌套相互关联,大伙试想下如果我们在 java 代码里构建一个枪的对象那是何其的复杂,假如我们要构造的不是简单的枪对象而是更加复杂的航空母舰,那么构造这个对象的成本之高是让人难以想象的,怎么来消除这种对象相互嵌套相互依赖的关系了?spring 提供了一种方式,这种方式就是 spring 提供一个容器,我们在 xml 文件里定义各个对象的依赖关系,由容器完成对象的构建,当我们 java 代码里需要使用某个实例的时候就可以从容器里获取,那么对象的构建操作就被 spring 容器接管,所以它被称为控制反转,控制反转的意思就是本来属于 java 程序里构建对象的功能交由容器接管,依赖注入就是当程序要使用某个对象时候,容器会把它注入到程序里,这就叫做依赖注入。在 java 开发里我们想使用某个类提供的功能,有两种方式,一种就是构造一个新的类,新的类继承该类,另一种方式则是将某个类定义在新类里,那么两个类之间就建立一种关联关系,spring 的 ioc 容器就是实现了这种关联关系(记住不是继承关系哦),那么某个类要被赋予到新类有哪些办法了?一般只有两种:一种就是通过构造函数,一种就是通过 setXXX 方式,这也是 spring 容器使用到了两种标准的注入方式。
不管是上面说的继承方式,还是关联方式其实都是增强目标对象能力的开发手段,在设计模式里有一种代理模式,代理模式将继承模式和关联模式结合在一起使用,代理模式就是继承模式和关联模式的综合体,不过这个综合体的作用倒不是解决对象注入的问题,而是为具体操作对象找到一个保姆或者是秘书,这就和小说里的二号首长一样,这个二号首长对外代表了具体的实例对象,实例对象的入口和出口都是通过这个二号首长,因为具体的实例对象是一号首长,一号首长是要干大事的,所以一些事务性,重复性的工作例如泡茶,安排车子,这样的工作是不用劳烦一号首长的大驾,而是二号首长帮忙解决的,这就是 aop 的思想,aop 解决程序开发里事务性,和核心业务无关的问题,但这些问题对于业务场景的实现是很有必要的,在实际开发里 aop 也是节省代码的一种方式。
Spring 的核心技术的作用本质就是一个 沟通机制,spring 总是尽全力的让沟通的双方信息畅通,同时降低双方的沟通成本,在现实机构里一个善于沟通的人肯定是该公司的领导,很会沟通的领导能调动起各种资源的积极性,善于沟通的领导就会做到海纳百川,让各种不同人追随他,所以当今的 spring 就是一个大框,什么都可以往里装。
Spring 很像银行,它不能直接创造物质财富,但是一切资源都要通过它进行流通,它能控制经济发展的走向,回到程序的世界,spring 的作用是被标榜为程序之间的解耦,spring 能降低不同模块之间的耦合度,原因就是在程序开发里不同模块之间信息的沟通是通过对象传递完成的,而对象能否顺利传递就是要合理的构建好对象,而管理好对象的构建方式就能管理好对象传递,这就是 spring 给系统架构设计带来的好处。
Lihuanghe
前端大牛能对后端理解如此,实在是太牛啦。
ioc 和 aop 是编程思想,放到前端,后端都是一样的。随着前端承担越来越多的业务,必然对 ioc 和 aop 有需要,但是究竟这个需要会到哪种程度这很难说。也许会有前端的 ioc,Aop 框架发展起来。
我补充一些:
tomcat 容器帮我们解决了从 http 字符流到 HttpServletRequest 对象的创建,随后交给 servlet 处理。随着 servlet 承载越来越多,越来越复杂的业务,一个 doGet,doPost 方法,两个 request,response 对象已远远不能满足需求了。
struts2,springMVC 这些框架为此提供了解决方案:把 HttpServletRequest 与 JavaBean 的一个 method 建立关系。根据 HttpServletRequest 创建对应的 JavaBean,生成方法参数对象,同时调用对应的 method 方法,接收 method 返回值,最后根据返回值创建 HttpServletresponse 对象交给容器。
框架做了这么多,只有一个目的:程序员只需要关心 method 方法怎么写(后端的 mvc 还要处理业务逻辑,数据持久化这些问题)。
以上这些是 OOP,IOC,AOP 协作一致来降低业务复杂度(解耦)。
话说回来。如果没有这么高的复杂度,如果不是 servlet 要承担越来越多的业务,鬼才会搞一个这么复杂的框架呢。
我觉得这是值得前端借鉴的,如果不是 ajax 流行,不是 js 性能的大幅提升,不是前端业务越来越复杂。我想也用不到 backbone, avalon 这些框架,简单的 jsp 页面或者 velocity 模版就能搞定了。
avalon 可能也要融入一些 aop 的思想。
Lihuanghe
我觉得前后端分离最大的问题在于,前后开端开发人员的知识结构差异大,思维方式也有很大差别。
体现在代码上,前后端的接口就是前端设计的 api 后端不好实现,后端给的 api 前端不好用。另外就是前后端的数据结构差异。“定接口”,“改接口” 是前后分离后最常遇见的问题。 现在 node 大行其道,淘宝不是用 node 搞一个前后分离的框架,让前端人员来编写部分后端代码来解决 “接口” 的问题。以前是后端人员兼写前端代码,淘宝这个框架是让前端人中兼写后端代码。是否存在一个中间状态呢?
如何让前后端的开发人中用同一种思维来设计 api,如何解决前后端数据结构对应是值得讨论的。
本作品采用 知识共享署名 - 相同方式共享 4.0 国际许可协议 进行许可。