SSH和SSM的區(qū)別,抽絲剝繭由內到外給你講清楚

SSH?通常指的是 Struts2 做前端控制器,Spring 管理各層的組件,Hibernate 負責持久化層。

SSM?則指的是 SpringMVC 做前端控制器,Spring 管理各層的組件,MyBatis 負責持久化層。

共同之處是都使用了Spring的依賴注入DI來管理各層的組件,使用了面向切面編程AOP來實現(xiàn)日志管理,權限認證,事務等通用功能的切入。

不同之處是 Struts2 和 SpringMVC 做前端控制器的區(qū)別,以及 Hibernate 和 MyBatis 做持久化時的區(qū)別。但是,Struts2 也可以和 MyBatis 搭配使用,SpringMVC 也可以和 Hibernate 搭配使用。本文為了簡化對比,指定 Struts2 要和 Hibernate 搭配,SpringMVC 要和 MyBatis 搭配。

1.1. SSH 和 SSM 的實現(xiàn)原理區(qū)別

所在分層SSHSSM頁面層(View)JSPJSP控制器層(Controller)Struts2SpringMVC業(yè)務層(Service)JavaJava持久層(DAO)HibernateMyBatis數(shù)據庫層(DB)MySQL/OracleMySQL/Oracle組件管理(Bean)SpringSpring

(1) Struts2 的原理

一個請求在Struts2框架中的處理大概分為以下幾個步驟:

1、客戶端初始化一個指向Servlet容器(例如Tomcat)的請求

2、這個請求經過一系列的過濾器(Filter)(這些過濾器中有一個叫做ActionContextCleanUp的可選過濾器,這個過濾器對于Struts2和其他框架的集成很有幫助,例如:SiteMesh Plugin)

3、接著FilterDispatcher被調用,F(xiàn)ilterDispatcher詢問ActionMapper來決定這個請求是否需要調用某個Action

FilterDispatcher是控制器的核心,就是mvc中c控制層的核心。下面粗略的分析下FilterDispatcher工作流程和原理:FilterDispatcher進行初始化并啟用核心doFilter。

publicvoiddoFilter(ServletRequest req, ServletResponse res, FilterChain chain)throwsIOException, ServletException{? ? ? ? showDeprecatedWarning();? ? ? ? HttpServletRequest request = (HttpServletRequest) req;? ? ? ? HttpServletResponse response = (HttpServletResponse) res;? ? ? ? ServletContext servletContext = getServletContext();? ? ? ? String timerKey ="FilterDispatcher_doFilter: ";try{//FIXME:this should be refactored better to not duplicate work with the action invocationValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack();? ? ? ? ? ? ActionContext ctx =newActionContext(stack.getContext());? ? ? ? ? ? ActionContext.setContext(ctx);? ? ? ? ? ? UtilTimerStack.push(timerKey);? ? ? ? ? ? request = prepareDispatcherAndWrapRequest(request, response);? ? ? ? ? ? ActionMapping mapping;try{//在這里找到Action的映射器mapping = actionMapper.getMapping(request, dispatcher.getConfigurationManager());? ? ? ? ? ? }catch(Exception ex) {? ? ? ? ? ? ? ? log.error("error getting ActionMapping", ex);? ? ? ? ? ? ? ? dispatcher.sendError(request, response, servletContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex);return;? ? ? ? ? ? }//沒有此Action的話,就去查找靜態(tài)資源if(mapping ==null) {// there is no action in this request, should we look for a static resource?String resourcePath = RequestUtils.getServletPath(request);if("".equals(resourcePath) &&null!= request.getPathInfo()) {? ? ? ? ? ? ? ? ? ? resourcePath = request.getPathInfo();? ? ? ? ? ? ? ? }if(staticResourceLoader.canHandle(resourcePath)) {? ? ? ? ? ? ? ? ? ? staticResourceLoader.findStaticResource(resourcePath, request, response);? ? ? ? ? ? ? ? }else{// this is a normal request, let it pass throughchain.doFilter(request, response);? ? ? ? ? ? ? ? }// The framework did its job herereturn;? ? ? ? ? ? }//有此Action的話則把控制權交給ActionProxydispatcher.serviceAction(request, response, servletContext, mapping);? ? ? ? }finally{? ? ? ? ? ? dispatcher.cleanUpRequest(request);try{? ? ? ? ? ? ? ? ActionContextCleanUp.cleanUp(req);? ? ? ? ? ? }finally{? ? ? ? ? ? ? ? UtilTimerStack.pop(timerKey);? ? ? ? ? ? }? ? ? ? ? ? devModeOverride.remove();? ? ? ? }? }

4、如果ActionMapper決定需要調用某個Action,F(xiàn)ilterDispatcher把請求的處理交給ActionProxy

publicvoidserviceAction(HttpServletRequest request, HttpServletResponse response, ServletContext context,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ActionMapping mapping) throws ServletException {? ? ? ? Map extraContext = createContextMap(request, response, mapping, context);// If there was a previous value stack, then create a new copy and pass it in to be used by the new ActionValueStack stack = (ValueStack) request.getAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY);booleannullStack = stack ==null;if(nullStack) {? ? ? ? ? ? ActionContext ctx = ActionContext.getContext();if(ctx !=null) {? ? ? ? ? ? ? ? stack = ctx.getValueStack();? ? ? ? ? ? }? ? ? ? }if(stack !=null) {? ? ? ? ? ? extraContext.put(ActionContext.VALUE_STACK, valueStackFactory.createValueStack(stack));? ? ? ? }StringtimerKey ="Handling request from Dispatcher";try{? ? ? ? ? ? UtilTimerStack.push(timerKey);Stringnamespace= mapping.getNamespace();Stringname = mapping.getName();Stringmethod = mapping.getMethod();//獲取配置文件Configuration config = configurationManager.getConfiguration();//根據配置文件找到此Action并生成ActionProxyActionProxy proxy = config.getContainer().getInstance(ActionProxyFactory.class).createActionProxy(namespace, name, method, extraContext,true,false);? ? ? ? ? ? request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack());// if the ActionMapping says to go straight to a result, do it!if(mapping.getResult() !=null) {? ? ? ? ? ? ? ? Result result = mapping.getResult();//ActionProxy創(chuàng)建一個ActionInvocation的實例result.execute(proxy.getInvocation());? ? ? ? ? ? }else{? ? ? ? ? ? ? ? proxy.execute();? ? ? ? ? ? }// If there was a previous value stack then set it back onto the requestif(!nullStack) {? ? ? ? ? ? ? ? request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack);? ? ? ? ? ? }? ? ? ? }catch(ConfigurationException e) {? ? ? ? ? ? logConfigurationException(request, e);? ? ? ? ? ? sendError(request, response, context, HttpServletResponse.SC_NOT_FOUND, e);? ? ? ? }catch(Exception e) {if(handleException || devMode) {? ? ? ? ? ? ? ? sendError(request, response, context, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);? ? ? ? ? ? }else{thrownewServletException(e);? ? ? ? ? ? }? ? ? ? }finally{? ? ? ? ? ? UtilTimerStack.pop(timerKey);? ? ? ? }? ? }

5、ActionProxy通過Configuration Manager詢問框架的配置文件,找到需要調用的Action類

6、ActionProxy創(chuàng)建一個ActionInvocation的實例。

7、ActionInvocation實例使用命名模式來調用,在調用Action的過程前后,涉及到相關攔截器(Intercepter)的調用。

8、一旦Action執(zhí)行完畢,ActionInvocation負責根據struts.xml中的配置找到對應的返回結果。返回結果通常是(但不總是,也可 能是另外的一個Action鏈)一個需要被表示的JSP或者FreeMarker的模版。

9、將處理結果返回給客戶端

(2) SpringMVC 的原理

執(zhí)行步驟:

第一步:發(fā)起請求到前端控制器(DispatcherServlet)

第二步:前端控制器請求HandlerMapping查找 Handler

可以根據xml配置、注解進行查找

第三步:處理器映射器HandlerMapping向前端控制器返回Handler

第四步:前端控制器調用處理器適配器去執(zhí)行Handler

第五步:處理器適配器去執(zhí)行Handler

第六步:Handler執(zhí)行完成給適配器返回ModelAndView

第七步:處理器適配器向前端控制器返回ModelAndView

ModelAndView是SpringMVC框架的一個底層對象,包括 Model和view

第八步:前端控制器請求視圖解析器去進行視圖解析

根據邏輯視圖名解析成真正的視圖(jsp)

第九步:視圖解析器向前端控制器返回View

第十步:前端控制器進行視圖渲染

視圖渲染將模型數(shù)據(在ModelAndView對象中)填充到request域

第十一步:前端控制器向用戶響應結果

(3) Hibernate 的原理

1.通過Configuration().configure();讀取并解析hibernate.cfg.xml配置文件

2.由hibernate.cfg.xml中的<mapping resource="com/xx/User.hbm.xml"/>讀取并解析映射信息

3.通過config.buildSessionFactory();//創(chuàng)建SessionFactory

4.sessionFactory.openSession();//打開Sesssion

5.session.beginTransaction();//創(chuàng)建事務Transation

6.persistent operate持久化操作

7.session.getTransaction().commit();//提交事務

8.關閉Session

9.關閉SesstionFactory

(4) MyBatis原理

MyBatis框架執(zhí)行過程:

1、配置MyBatis的配置文件,SqlMapConfig.xml(名稱不固定)

2、通過配置文件,加載MyBatis運行環(huán)境,創(chuàng)建SqlSessionFactory會話工廠

SqlSessionFactory 在實際使用時按單例方式。

3、通過SqlSessionFactory創(chuàng)建SqlSession

SqlSession 是一個面向用戶接口(提供操作數(shù)據庫方法),實現(xiàn)對象是線程不安全的,建議sqlSession應用場合在方法體內。

4、調用 sqlSession 的方法去操作數(shù)據。

如果需要提交事務,需要執(zhí)行 SqlSession 的 commit() 方法。

5、釋放資源,關閉SqlSession

1.2. Struts2 和 SpringMVC 在 web.xml 中配置的不同

(1) Struts2

struts2org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilterfilterConfigclasspath:struts2/struts.xmlstruts2/*

Struts2使用Filter嵌入自己的框架。配置文件加載順序為:default.properties -> struts-default.xml -> struts-plugins.xml -> struts.xml -> struts.locale。

加載順序可以參考這篇文章的源碼分析了解更多。https://my.oschina.net/gschen/blog/121433

(2) SpringMVC

<!-- springmvc前端控制器,rest配置 -->springmvc_restorg.springframework.web.servlet.DispatcherServlet<!-- contextConfigLocation配置springmvc加載的配置文件(配置處理器映射器、適配器等等) 如果不配置contextConfigLocation,默認加載的是/WEB-INF/servlet名稱-serlvet.xml(springmvc-servlet.xml) -->contextConfigLocationclasspath:spring/springmvc.xmlspringmvc_rest/

SpringMVC使用Servlet嵌入自己的框架。

(3)web.xml不同之處

SpringMVC的入口是Servlet,而Struts2是Filter(這里要指出,F(xiàn)ilter和Servlet是不同的。以前認為filter是servlet的一種特殊),這就導致了二者的機制不同,這里就牽涉到Servlet和Filter的區(qū)別了。但是這只是接管用戶請求的兩種不同方式而已,控制權被Struts2和SpringMVC掌握之后,想做什么事都是可以做到的。

Servlet

servlet是一種運行服務器端的java應用程序,具有獨立于平臺和協(xié)議的特性,并且可以動態(tài)的生成web頁面,它工作在客戶端請求與服務器響應的中間層。最早支持 Servlet 技術的是 JavaSoft 的 Java Web Server。此后,一些其它的基于 Java 的 Web Server 開始支持標準的 Servlet API。Servlet 的主要功能在于交互式地瀏覽和修改數(shù)據,生成動態(tài) Web 內容。這個過程為:

1)客戶端發(fā)送請求至服務器端;2)服務器將請求信息發(fā)送至Servlet;3)Servlet生成響應內容并將其傳給服務器。響應內容動態(tài)生成,通常取決于客戶端的請求;4)服務器將響應返回給客戶端。在Web應用程序中,一個Servlet在一個時刻可能被多個用戶同時訪問。這時Web容器將為每個用戶創(chuàng)建一個線程來執(zhí)行Servlet。如果Servlet不涉及共享資源的問題,不必關心多線程問題。但如果Servlet需要共享資源,需要保證Servlet是線程安全的。為了簡化開發(fā)流程,Servlet3.0引入了注解(annotation),這使得web部署描述符web.xml不再是必須的選擇

Filter:Filter是一個可以復用的代碼片段,可以用來轉換HTTP請求、響應和頭信息。Filter不像Servlet,它不能產生一個請求或者響應,它只是修改對某一資源的請求,或者修改從某一的響應。Servlet中的過濾器Filter是實現(xiàn)了javax.servlet.Filter接口的服務器端程序,主要的用途是過濾字符編碼、做一些業(yè)務邏輯判斷等。其工作原理是,只要你在web.xml文件配置好要攔截的客戶端請求,它都會幫你攔截到請求,此時你就可以對請求或響應(Request、Response)統(tǒng)一設置編碼,簡化操作;同時還可進行邏輯判斷,如用戶是否已經登陸、有沒有權限訪問該頁面等等工作。它是隨你的web應用啟動而啟動的,只初始化一次,以后就可以攔截相關請求,只有當你的web應用停止或重新部署的時候才銷毀。Filter可認為是Servlet的一種“變種”,它主要用于對用戶請求進行預處理,也可以對HttpServletResponse進行后處理,是個典型的處理鏈。它與Servlet的區(qū)別在于:它不能直接向用戶生成響應。完整的流程是:Filter對用戶請求進行預處理,接著將請求交給Servlet進行處理并生成響應,最后Filter再對服務器響應進行后處理。

1.3. Struts2 和 SpringMVC 處理用戶請求的不同

Struts2和SpringMVC的核心都是接管用戶的請求,解決傳統(tǒng)Servlet開發(fā)過于繁瑣,重用性不高的問題。

Struts2和SpringMVC都有注解和配置文件兩種匹配用戶請求URL的方式。

Struts2注解方式匹配URL

首先需要將架包(struts2-convention-plugin-xxx.jar)導入工程中

示例

packagecom.example.actions;importcom.opensymphony.xwork2.ActionSupport;importorg.apache.struts2.convention.annotation.Action;importorg.apache.struts2.convention.annotation.Actions;importorg.apache.struts2.convention.annotation.Result;importorg.apache.struts2.convention.annotation.Results;@Results({@Result(name="failure", location="fail.jsp")? })? public class HelloWorld extends ActionSupport {@Action(value="/different/url",? ? ? results={@Result(name="success", location="http://struts.apache.org", type="redirect")}? ? )? ? public String execute()? {returnSUCCESS;? ? }@Action("/another/url")? ? public String doSomething()? {returnSUCCESS;? ? }? }

Struts2配置方式匹配URL

/fail.jsphttp://struts.apache.org/fail.jsp

SpringMVC注解方式匹配URL

packagecom.jpkc.controller;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjavax.servlet.http.HttpSession;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RequestMethod;importorg.springframework.web.servlet.ModelAndView;@RequestMapping("/admin")@ControllerpublicclassLoginController{@RequestMapping("/admin_home")publicString admin_home() throws Exception? ? {return"forward:/shop/index.jsp";? ? }@RequestMapping("/exit")publicString logout(ModelAndView model, HttpSession session) throws Exception? ? {? ? ? ? session.invalidate();return"redirect:/manager/login.jsp";? ? }}

SpringMVC配置方式匹配URL

publicclassItemsController1implementsController{@OverridepublicModelAndViewhandleRequest(HttpServletRequest request,

? ? ? ? ? ? HttpServletResponse response)throwsException{//調用Service查找 數(shù)據庫,查詢商品列表,這里使用靜態(tài)數(shù)據模擬List itemsList =newArrayList();//向list中填充靜態(tài)數(shù)據Items items_1 =newItems();? ? ? ? items_1.setName("聯(lián)想筆記本");? ? ? ? items_1.setPrice(6000f);? ? ? ? items_1.setDetail("ThinkPad T430 聯(lián)想筆記本電腦!");? ? ? ? Items items_2 =newItems();? ? ? ? items_2.setName("蘋果手機");? ? ? ? items_2.setPrice(5000f);? ? ? ? items_2.setDetail("iphone6蘋果手機!");? ? ? ? itemsList.add(items_1);? ? ? ? itemsList.add(items_2);//返回ModelAndViewModelAndView modelAndView =newModelAndView();//相當 于request的setAttribut,在jsp頁面中通過itemsList取數(shù)據modelAndView.addObject("itemsList", itemsList);//指定視圖modelAndView.setViewName("/WEB-INF/jsp/items/itemsList.jsp");returnmodelAndView;? ? }}

<!-- 配置Handler -->

1、Struts2是類級別的攔截, 一個類對應一個request上下文,SpringMVC是方法級別的攔截,一個方法對應一個request上下文,而方法同時又跟一個url對應,所以說從架構本身上SpringMVC就容易實現(xiàn)restful url,而struts2的架構實現(xiàn)起來要費勁,因為Struts2中Action的一個方法可以對應一個url,而其類屬性卻被所有方法共享,這也就無法用注解或其他方式標識其所屬方法了。

2、由上邊原因,SpringMVC的方法之間基本上獨立的,獨享request response數(shù)據,請求數(shù)據通過參數(shù)獲取,處理結果通過ModelMap交回給框架,方法之間不共享變量,而Struts2搞的就比較亂,雖然方法之間也是獨立的,但其所有Action變量是共享的,這不會影響程序運行,卻給我們編碼 讀程序時帶來麻煩,每次來了請求就創(chuàng)建一個Action,一個Action對象對應一個request上下文。

3、由于Struts2需要針對每個request進行封裝,把request,session等servlet生命周期的變量封裝成一個一個Map,供給每個Action使用,并保證線程安全,所以在原則上,是比較耗費內存的。

1.4. Struts2 和 SpringMVC 實現(xiàn) RESTful 的不同

實現(xiàn)上面這個鏈接,其中l(wèi)ocalhost是域名,jpkc是項目名。

Struts2實現(xiàn)方式

/story/story_02.jsp

publicclassCourseActionextendsActionSupport{publicStringget_course_info(){? ? ? ? String actionName = ServletActionContext.getActionMapping().getName();? ? ? ? CourseInfo courseInfoFromDB = courseInfoDAO.findById(actionName);if(courseInfoFromDB ==null)? ? ? ? {return"404";? ? ? ? }? ? ? ? Course courseFromDB = courseDAO.findById(actionName);if(courseFromDB ==null)? ? ? ? {return"404";? ? ? ? }? ? ? ? setCourseInfo(courseInfoFromDB);? ? ? ? setCourse(courseFromDB);returnSUCCESS;? ? }}

SpringMVC實現(xiàn)方式

@ControllerpublicclassCourseController{@RequestMapping("/item/{id}")publicModelAndView get_course_info(ModelAndView model,@PathVariable("id")String id)? ? {if(CM.validIsEmptyWithTrim(id))? ? ? ? {? ? ? ? ? ? model.addObject("message","沒有找到此視頻頁面");? ? ? ? ? ? model.setViewName("/WEB-INF/jsp/error");returnmodel;? ? ? ? }? ? ? ? CourseInfo courseInfoFromDB=null;try{? ? ? ? ? ? courseInfoFromDB = courseInfoService.selectByPrimaryKey(id);? ? ? ? }catch(Exception e1)? ? ? ? {? ? ? ? ? ? System.out.println("沒有找到課程信息");? ? ? ? }if(courseInfoFromDB ==null)? ? ? ? {? ? ? ? ? ? model.addObject("message","沒有找到此視頻頁面");? ? ? ? ? ? model.setViewName("/WEB-INF/jsp/error");returnmodel;? ? ? ? }? ? ? ? Course courseFromDB =null;try{? ? ? ? ? ? courseFromDB = courseService.selectByPrimaryKey(id);? ? ? ? }catch(Exception e)? ? ? ? {? ? ? ? ? ? System.out.println("沒有查找到課程");? ? ? ? }if(courseFromDB ==null)? ? ? ? {? ? ? ? ? ? model.addObject("message","沒有找到此視頻頁面");? ? ? ? ? ? model.setViewName("/WEB-INF/jsp/error");returnmodel;? ? ? ? }? ? ? ? model.addObject("courseInfo", courseInfoFromDB);? ? ? ? model.addObject("course", courseFromDB);? ? ? ? model.setViewName("/story/story_02");returnmodel;? ? }}

對于類似于http://localhost/jpkc/item/id1這種鏈接,Struts2實現(xiàn)RESTful風格需要在代碼中調用ServletActionContext.getActionMapping().getName()獲取ActionName。SpringMVC直接將鏈接映射到方法參數(shù)里去了。

如果類似于http://localhost/jpkc/id2/id1這種鏈接,Struts2要進一步分析鏈接得到id1和id2。SpringMVC依然可以將id2映射到方法參數(shù)上。從調用的角度來看SpringMVC要方便一些。但是如果將Struts2獲取方式封裝一下,也可以得到同樣的效果。

1.5. Struts2 和 SpringMVC 獲取 request 參數(shù)的不同

前臺頁面有一個表單需要提交。

Struts2 接收 request 參數(shù)

登錄系統(tǒng)請輸入用戶名和密碼用戶名密碼登錄

packagecom.jpkc.pojo;importjava.io.Serializable;publicclassAccountimplementsSerializable{privateString id;privateString password;privateString name;publicAccount(){super();// TODO Auto-generated constructor stub}publicStringgetId(){returnid;? ? }publicvoidsetId(String id){this.id = id;? ? }publicStringgetPassword(){returnpassword;? ? }publicvoidsetPassword(String password){this.password = password;? ? }publicStringgetName(){returnname;? ? }publicvoidsetName(String name){this.name = name;? ? }}

packagecom.jpkc.action;importjava.util.HashMap;importjava.util.Map;importcom.jpkc.common.CM;importcom.jpkc.pojo.Account;publicclassAccountActionextendsBaseAction{privateAccount account;publicStringlogin_do(){? ? ? ? String method = getRequest().getMethod();if(method.toUpperCase().equals("GET"))? ? ? ? {return"404";? ? ? ? }if(account ==null|| CM.validIsEmpty(account.getId()) || CM.validIsEmpty(account.getPassword()))? ? ? ? {returnERROR;? ? ? ? }? ? ? ? getSession().setAttribute("accountSession", account);returnSUCCESS;? ? }publicAccountgetAccount(){returnaccount;? ? }publicvoidsetAccount(Account account){this.account = account;? ? }}

SpringMVC 接收 request 參數(shù)

登錄系統(tǒng)請輸入用戶名和密碼用戶名密碼登錄

packagecom.jpkc.pojo;importjava.io.Serializable;publicclassAccountimplementsSerializable{privateString id;privateString password;privateString name;publicAccount(){super();// TODO Auto-generated constructor stub}publicStringgetId(){returnid;? ? }publicvoidsetId(String id){this.id = id;? ? }publicStringgetPassword(){returnpassword;? ? }publicvoidsetPassword(String password){this.password = password;? ? }publicStringgetName(){returnname;? ? }publicvoidsetName(String name){this.name = name;? ? }}

package com.jpkc.controller;importjava.util.HashMap;importjava.util.Map;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjavax.servlet.http.HttpSession;importorg.codehaus.jackson.map.ObjectMapper;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RequestMethod;importorg.springframework.web.servlet.ModelAndView;importcom.jpkc.common.CM;importcom.jpkc.exception.CustomException;importcom.jpkc.mapper.CourseInfoMapper;importcom.jpkc.pojo.Account;importcom.jpkc.pojo.CourseInfo;importcom.jpkc.service.LoginService;@RequestMapping("/admin")@ControllerpublicclassLoginController{? ? @AutowiredLoginServiceloginService;? ? @RequestMapping(value ="/login_do", method = {RequestMethod.POST})publicvoid login_do(ModelAndViewmodel,HttpServletRequestrequest,HttpServletResponseresponse,HttpSessionsession,Accountaccount)throwsException{? ? ? ? response.setCharacterEncoding("utf-8");? ? ? ? response.setContentType("application/json;charset=utf-8");Map json = newHashMap();Stringinfo;if(account == null ||CM.validIsEmpty(account.getId()) ||CM.validIsEmpty(account.getPassword()))? ? ? ? {? ? ? ? ? ? info ="用戶名、密碼都是必填項。";? ? ? ? ? ? json.put("success",false);? ? ? ? ? ? json.put("info", info);? ? ? ? ? ? response.getWriter().write(newObjectMapper().writeValueAsString(json));return;? ? ? ? }? ? ? ? session.setAttribute("accountSession", account);? ? ? ? json.put("success",true);? ? ? ? response.getWriter().write(newObjectMapper().writeValueAsString(json));return;? ? }}

Struts2單個方法可以處理一個request,接收參數(shù)Account需要定義一個成員變量,Struts2會自動將對應的參數(shù)調用成員變量的set方法設置進去。處理方法可以在方法內獲取到。用完還存在request級別Map中。

SpringMVC的單個方法也對應于一個request,接收參數(shù)Account需要定義一個方法參數(shù),SpringMVC會自動將對應的參數(shù)設置到方法參數(shù)中去。處理方法可以在方法內獲取到。用完即銷毀。

可以看出兩種框架都可以實現(xiàn)參數(shù)的自動轉換。Struts2定義一個成員變量,其他方法都是可以共享的,不用重新定義。SpringMVC每個方法都是獨立的,方法參數(shù)是每一個方法獨享的。

各有利弊。

成員變量共享可以避免重復定義,但是方法一多,用到的成員變量原來越多,整個Action類會慘不忍睹,因為你不知道其中一個方法具體會用到哪幾個成員變量。而且用不到的成員變量也被存儲到request級別Map中了。造成內存的浪費。

方法參數(shù)是方法獨享的。則不能復用到其他方法,但是對于當前方法來說有哪些參數(shù)足夠明確,而且不用和其他方法攪合,干脆利落。

從JVM角度來說,Struts2成員變量會被分配到堆中。SpringMVC方法參數(shù)則會存在于方法棧中,一般認為棧比堆更輕量一些,方法結束,用完參數(shù)即回收。堆需要垃圾回收觸發(fā)時才能統(tǒng)一回收。

1.6. Struts2 和 SpringMVC 限制訪問方式GET和POST的不同

在上例中,表單提交有密碼,需要指定只接受POST提交方式。

Struts2指定POST方式

publicStringlogin_do(){? ? ? ? String method = getRequest().getMethod();if(method.toUpperCase().equals("GET"))? ? ? ? {return"404";? ? ? ? }? ? }

SpringMVC指定POST方式

@RequestMapping(value ="/login_do", method = { RequestMethod.POST })publicvoidlogin_do(ModelAndView model, HttpServletRequest request, HttpServletResponse response, HttpSession session, Account account) throws Exception? ? {? ? ? ? response.setCharacterEncoding("utf-8");? ? ? ? response.setContentType("application/json;charset=utf-8");? ? ? ? Map json =newHashMap();Stringinfo;if(account ==null|| CM.validIsEmpty(account.getId()) || CM.validIsEmpty(account.getPassword()))? ? ? ? {? ? ? ? ? ? info ="用戶名、密碼都是必填項。";? ? ? ? ? ? json.put("success",false);? ? ? ? ? ? json.put("info", info);? ? ? ? ? ? response.getWriter().write(newObjectMapper().writeValueAsString(json));return;? ? ? ? }? ? ? ? session.setAttribute("accountSession", account);? ? ? ? json.put("success",true);? ? ? ? response.getWriter().write(newObjectMapper().writeValueAsString(json));return;? ? }

Struts2限制只能通過POST方式訪問,是通過調用request的getMethod方法來得到當前訪問方式。然后手工的去判斷。

SpringMVC也可以調用request的getMethod方法來判斷,但是框架本身提供了方便的內置判斷。使用注解即可。

Struts2通過攔截器設置好訪問方式的代碼后,也可以通過注解的方式指定攔截器得到同樣的效果。本身不是太難的事情,兩個框架都可以實現(xiàn),Struts2需要手工實現(xiàn),SpringMVC默認提供了。即使SpringMVC不提供,調用SpringMVC的攔截器也能和Struts2的攔截器的效果一樣。在GET和POST訪問限制方面,并沒有誰優(yōu)誰劣,都可以實現(xiàn)。只是SpringMVC愿意往前多走一小步。

1.7. Struts2 和 SpringMVC 攔截器的不同

后臺頁面需要登錄,我們可以使用攔截器限制未登錄的用戶訪問。

Struts2實現(xiàn)攔截器的方式

publicclassManagerLoginInterceptorextendsAbstractInterceptor{? ? @OverridepublicStringintercept(ActionInvocation invocation) throws Exception{? ? ? ? String actionName = ServletActionContext.getActionMapping().getName();// 如果是登錄、注冊、退出的話就不要攔截了if(actionName.equals("exit") || actionName.equals("login") || actionName.equals("login_do") || actionName.equals("regist")? ? ? ? ? ? ? ? || actionName.equals("regist_do"))? ? ? ? {returninvocation.invoke();? ? ? ? }// 如果不是管理員就不能進入Manager managerTemp = (Manager) ServletActionContext.getRequest().getSession().getAttribute("managerSession");if(managerTemp ==null)? ? ? ? {return"manager_login";? ? ? ? }//驗證成功,放行。returninvocation.invoke();? ? }}

<!-- 自定義攔截器棧-攔截未登錄的管理員- -->json

Struts2還提供了很多默認的攔截器供用戶調用。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

SpringMVC實現(xiàn)攔截器的方式

publicclassLoginInterceptorimplementsHandlerInterceptor{// 進入 Handler方法之前執(zhí)行// 用于身份認證、身份授權// 比如身份認證,如果認證通過表示當前用戶沒有登陸,需要此方法攔截不再向下執(zhí)行@OverridepublicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throwsException{// 獲取請求的urlString url = request.getRequestURI();// 判斷url是否是公開 地址(實際使用時將公開 地址配置配置文件中)// 這里公開地址是登陸提交的地址if(url.indexOf("login") >=0|| url.indexOf("exit") >=0)? ? ? ? {// 如果進行登陸提交,放行returntrue;? ? ? ? }// 判斷sessionHttpSession session = request.getSession();// 從session中取出用戶身份信息Account account = (Account) session.getAttribute("accountSession");if(account !=null)? ? ? ? {// 身份存在,放行returntrue;? ? ? ? }// 執(zhí)行這里表示用戶身份需要認證,跳轉登陸頁面request.getRequestDispatcher("/manager/login.jsp").forward(request, response);// return false表示攔截,不向下執(zhí)行// return true表示放行returnfalse;? ? }// 進入Handler方法之后,返回modelAndView之前執(zhí)行// 應用場景從modelAndView出發(fā):將公用的模型數(shù)據(比如菜單導航)在這里傳到視圖,也可以在這里統(tǒng)一指定視圖@OverridepublicvoidpostHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)throwsException{? ? ? ? System.out.println("HandlerInterceptor1...postHandle");? ? }// 執(zhí)行Handler完成執(zhí)行此方法// 應用場景:統(tǒng)一異常處理,統(tǒng)一日志處理@OverridepublicvoidafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throwsException{? ? ? ? System.out.println("HandlerInterceptor1...afterCompletion");? ? }}

<!--攔截器 --><!--多個攔截器,順序執(zhí)行 --><!-- 登錄認證攔截器 -->

攔截器實現(xiàn)機制上,Struts2有自己的interceptor機制,SpringMVC用的是獨立的AOP方式。都可以實現(xiàn)在前后進行攔截。

1.8. Struts2 和 SpringMVC 支持 JSON 的不同

有時我們界面的一些操作,是通過 Ajax 調用后臺的服務,獲取服務器返回的 json 數(shù)據,進行后續(xù)的操作。

Struts2 實現(xiàn)JSON數(shù)據返回的方式

<!-- 這里指定將被Struts2序列化的屬性,該屬性在action中必須有對應的getter方法 -->json

publicclassAccountActionextendsBaseAction{// 常用變量privateMap json;// 返回到前臺的map對象privateAccount account;publicAccountAction()? ? {? ? ? ? json =newHashMap();? ? }publicStringlogin_do()? ? {if(account ==null|| CM.validIsEmpty(account.getId()) || CM.validIsEmpty(account.getPassword()))? ? ? ? {? ? ? ? ? ? info ="用戶名、密碼都是必填項。";? ? ? ? ? ? json.put("success",false);? ? ? ? ? ? json.put("info", info);returnSUCCESS;? ? ? ? }? ? ? ? getSession().setAttribute("accountSession", account);? ? ? ? json.put("success",true);returnSUCCESS;? ? }}

$.post("login_do", $(".login-form").serialize(),function(json){if(json.success ==true)? ? {window.location.href="shop/index.jsp";? ? }else{? ? ? ? alert("操作失?。?+ json.info);? ? }},"json");

SpringMVC 實現(xiàn)JSON數(shù)據返回的方式

<!--注解適配器 -->

$.post("login_do", $(".login-form").serialize(),function(json){if(json.success ==true)? ? {window.location.href="shop/index.jsp";? ? }else{? ? ? ? alert("操作失?。?+ json.info);? ? }},"json");

SpringMVC在控制器中返回json有兩種方式。

一種是使用response返回json。

@RequestMapping(value ="/login_do", method = { RequestMethod.POST })publicvoidlogin_do(ModelAndView model, HttpServletRequest request, HttpServletResponse response, HttpSession session, Account account) throws Exception? ? {? ? ? ? response.setCharacterEncoding("utf-8");? ? ? ? response.setContentType("application/json;charset=utf-8");? ? ? ? Map json =newHashMap();Stringinfo;if(account ==null|| CM.validIsEmpty(account.getId()) || CM.validIsEmpty(account.getPassword()))? ? ? ? {? ? ? ? ? ? info ="用戶名、密碼都是必填項。";? ? ? ? ? ? json.put("success",false);? ? ? ? ? ? json.put("info", info);? ? ? ? ? ? response.getWriter().write(newObjectMapper().writeValueAsString(json));return;? ? ? ? }? ? ? ? session.setAttribute("accountSession", account);? ? ? ? json.put("success",true);? ? ? ? response.getWriter().write(newObjectMapper().writeValueAsString(json));return;? ? }

另一種是使用@ResponseBody注解方式。

@RequestMapping(value ="/login_do", method = { RequestMethod.POST })public@ResponseBodyMap login_do(ModelAndView model, HttpServletRequest request, HttpServletResponse response, HttpSession session, Account account) throws Exception? ? {? ? ? ? Map json =newHashMap();Stringinfo;if(account ==null|| CM.validIsEmpty(account.getId()) || CM.validIsEmpty(account.getPassword()))? ? ? ? {? ? ? ? ? ? info ="用戶名、密碼都是必填項。";? ? ? ? ? ? json.put("success",false);? ? ? ? ? ? json.put("info", info);returnjson;? ? ? ? }? ? ? ? session.setAttribute("accountSession", account);? ? ? ? json.put("success",true);returnjson;? ? }

可以看出,Struts2 和 SpringMVC 都可以實現(xiàn) Ajax 請求返回 JSON。實現(xiàn)方式上,Struts2在配置文件配置返回類型為JSON。SpringMVC在方法上加一個@ResponseBody注解即可返回對應類型轉成的JSON字符串。都是對返回數(shù)據轉成JSON,但是不得不說SpringMVC的寫法方便太多了。

1.9. Hibernate 和 MyBatis 在 ORM 側重點的不同

Hibernate對數(shù)據庫結構提供了較為完整的封裝,Hibernate的O/R Mapping實現(xiàn)了POJO 和數(shù)據庫表之間的映射,以及SQL 的自動生成和執(zhí)行。程序員往往只需定義好了POJO 到數(shù)據庫表的映射關系,即可通過Hibernate 提供的方法完成持久層操作。程序員甚至不需要對SQL 的熟練掌握,?Hibernate/OJB 會根據指定的存儲邏輯,自動生成對應的SQL?并調用JDBC 接口加以執(zhí)行。

MyBatis 的著力點,則在于POJO 與SQL之間的映射關系。然后通過映射配置文件,將SQL所需的參數(shù),以及返回的結果字段映射到指定POJO。 相對Hibernate“O/R”而言,MyBatis 是一種“Sql Mapping”的ORM實現(xiàn)。

SQL語句支持:Hibernate可以完全不用手寫SQL語句,MyBatis手動維護SQL語句。Hibernate修改優(yōu)化SQL語句困難,MyBatis由于SQL語句自己控制,優(yōu)化非常方便。

開發(fā)速度:Hibernate的真正掌握要比Mybatis來得難些。Mybatis框架相對簡單很容易上手,但也相對簡陋些。

開發(fā)社區(qū):Hibernate 與Mybatis都是流行的持久層開發(fā)框架,但Hibernate開發(fā)社區(qū)相對多熱鬧些,支持的工具也多,更新也快。而Mybatis相對平靜,工具較少。

開發(fā)工作量:Hibernate和MyBatis都有相應的代碼生成工具??梢陨珊唵位镜腄AO層方法。

針對高級查詢,Mybatis需要手動編寫SQL語句,以及ResultMap。而Hibernate有良好的映射機制,開發(fā)者無需關心SQL的生成與結果映射,可以更專注于業(yè)務流程。

1.10. Hibernate 和 MyBatis 在調優(yōu)方面的不同

制定合理的緩存策略;

盡量使用延遲加載特性;

采用合理的Session管理機制;

SQL優(yōu)化方面

Hibernate的查詢會將表中的所有字段查詢出來,這一點會有性能消耗。Hibernate也可以自己寫SQL來指定需要查詢的字段,但這樣就破壞了Hibernate開發(fā)的簡潔性。而Mybatis的SQL是手動編寫的,所以可以按需求指定查詢的字段。

Hibernate HQL語句的調優(yōu)需要將SQL打印出來,而Hibernate的SQL被很多人嫌棄因為太丑了。MyBatis的SQL是自己手動寫的所以調整方便。但Hibernate具有自己的日志統(tǒng)計。Mybatis本身不帶日志統(tǒng)計,使用Log4j進行日志記錄。

擴展性方面

Hibernate與具體數(shù)據庫的關聯(lián)只需在XML文件中配置即可,所有的HQL語句與具體使用的數(shù)據庫無關,移植性很好。MyBatis項目中所有的SQL語句都是依賴所用的數(shù)據庫的,所以不同數(shù)據庫類型的支持不好。

1.11. Hibernate 和 MyBatis 在對象管理與抓取策略的不同

對象管理

Hibernate 是完整的對象/關系映射解決方案,它提供了對象狀態(tài)管理(state management)的功能,使開發(fā)者不再需要理會底層數(shù)據庫系統(tǒng)的細節(jié)。也就是說,相對于常見的 JDBC/SQL 持久層方案中需要管理 SQL 語句,Hibernate采用了更自然的面向對象的視角來持久化 Java 應用中的數(shù)據。

換句話說,使用 Hibernate 的開發(fā)者應該總是關注對象的狀態(tài)(state),不必考慮 SQL 語句的執(zhí)行。這部分細節(jié)已經由 Hibernate 掌管妥當,只有開發(fā)者在進行系統(tǒng)性能調優(yōu)的時候才需要進行了解。

而MyBatis在這一塊沒有文檔說明,用戶需要對對象自己進行詳細的管理。當調用sqlSession.commit()方法時才會進行真正的提交。

抓取策略

Hibernate對實體關聯(lián)對象的抓取有著良好的機制。對于每一個關聯(lián)關系都可以詳細地設置是否延遲加載,并且提供關聯(lián)抓取、查詢抓取、子查詢抓取、批量抓取四種模式。 它是詳細配置和處理的。

而Mybatis的延遲加載是全局配置的,在resultMap中使用association中的select指定延遲加載去執(zhí)行的statement的id。

<!-- 延遲加載的resultMap --><!--對訂單信息進行映射配置? --><!-- 實現(xiàn)對用戶信息進行延遲加載 -->

1.12. Hibernate 和 MyBatis 在緩存機制的不同

Hibernate緩存

Hibernate一級緩存是Session緩存,利用好一級緩存就需要對Session的生命周期進行管理好。建議在一個Action操作中使用一個Session。一級緩存需要對Session進行嚴格管理。

Hibernate二級緩存是SessionFactory級的緩存。 SessionFactory的緩存分為內置緩存和外置緩存。內置緩存中存放的是SessionFactory對象的一些集合屬性包含的數(shù)據(映射元素據及預定SQL語句等),對于應用程序來說,它是只讀的。外置緩存中存放的是數(shù)據庫數(shù)據的副本,其作用和一級緩存類似.二級緩存除了以內存作為存儲介質外,還可以選用硬盤等外部存儲設備。二級緩存稱為進程級緩存或SessionFactory級緩存,它可以被所有session共享,它的生命周期伴隨著SessionFactory的生命周期存在和消亡。

MyBatis緩存

MyBatis 包含一個非常強大的查詢緩存特性,它可以非常方便地配置和定制。MyBatis 3 中的緩存實現(xiàn)的很多改進都已經實現(xiàn)了,使得它更加強大而且易于配置。

一級緩存是SqlSession級別的緩存,二級緩存是mapper(命名空間)級別的緩存,默認情況下是沒有開啟二級緩存的。

要開啟二級緩存,你需要在你的 SQL 映射文件中添加一行: <cache/>

字面上看就是這樣。這個簡單語句的效果如下:

映射語句文件中的所有 select 語句將會被緩存。

映射語句文件中的所有 insert,update 和 delete 語句會刷新緩存。

緩存會使用 Least Recently Used(LRU,最近最少使用的)算法來收回。

根據時間表(比如 no Flush Interval,沒有刷新間隔), 緩存不會以任何時間順序 來刷新。

緩存會存儲列表集合或對象(無論查詢方法返回什么)的 1024 個引用。

緩存會被視為是 read/write(可讀/可寫)的緩存,意味著對象檢索不是共享的,而 且可以安全地被調用者修改,而不干擾其他調用者或線程所做的潛在修改。

所有的這些屬性都可以通過緩存元素的屬性來修改。

比如: <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

這個更高級的配置創(chuàng)建了一個 FIFO 緩存,并每隔 60 秒刷新,存數(shù)結果對象或列表的 512 個引用,而且返回的對象被認為是只讀的,因此在不同線程中的調用者之間修改它們會 導致沖突。可用的收回策略有, 默認的是 LRU:

LRU – 最近最少使用的:移除最長時間不被使用的對象。

FIFO – 先進先出:按對象進入緩存的順序來移除它們。

SOFT – 軟引用:移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象。

WEAK – 弱引用:更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象。

flushInterval(刷新間隔)可以被設置為任意的正整數(shù),而且它們代表一個合理的毫秒 形式的時間段。默認情況是不設置,也就是沒有刷新間隔,緩存僅僅調用語句時刷新。

size(引用數(shù)目)可以被設置為任意正整數(shù),要記住你緩存的對象數(shù)目和你運行環(huán)境的 可用內存資源數(shù)目。默認值是1024。

readOnly(只讀)屬性可以被設置為 true 或 false。只讀的緩存會給所有調用者返回緩 存對象的相同實例。因此這些對象不能被修改。這提供了很重要的性能優(yōu)勢。可讀寫的緩存 會返回緩存對象的拷貝(通過序列化) 。這會慢一些,但是安全,因此默認是 false。

相同點

Hibernate和Mybatis的二級緩存除了采用系統(tǒng)默認的緩存機制外,都可以通過實現(xiàn)你自己的緩存或為其他第三方緩存方案,創(chuàng)建適配器來完全覆蓋緩存行為。

不同點

Hibernate的二級緩存配置在SessionFactory生成的配置文件中進行詳細配置,然后再在具體的表-對象映射中配置是那種緩存。

MyBatis的二級緩存配置都是在每個具體的表-對象映射中進行詳細配置,這樣針對不同的表可以自定義不同的緩存機制。并且Mybatis可以在命名空間中共享相同的緩存配置和實例,通過Cache-ref來實現(xiàn)。

兩者比較

因為Hibernate對查詢對象有著良好的管理機制,用戶無需關心SQL。所以在使用二級緩存時如果出現(xiàn)臟數(shù)據,系統(tǒng)會報出錯誤并提示。

而MyBatis在這一方面,使用二級緩存時需要特別小心。如果不能完全確定數(shù)據更新操作的波及范圍,避免Cache的盲目使用。否則,臟數(shù)據的出現(xiàn)會給系統(tǒng)的正常運行帶來很大的隱患。

1.13. Hibernate 和 MyBatis 對比總結

兩者相同點

Hibernate與MyBatis都可以是通過SessionFactoryBuider由XML配置文件生成SessionFactory,然后由SessionFactory 生成Session,最后由Session來開啟執(zhí)行事務和SQL語句。其中SessionFactoryBuider,SessionFactory,Session的生命周期都是差不多的。

Hibernate和MyBatis都支持JDBC和JTA事務處理。

Mybatis優(yōu)勢

MyBatis可以進行更為細致的SQL優(yōu)化,可以減少查詢字段。

MyBatis容易掌握,而Hibernate門檻較高。

Hibernate優(yōu)勢

Hibernate的DAO層開發(fā)比MyBatis簡單,Mybatis需要維護SQL和結果映射。

Hibernate對對象的維護和緩存要比MyBatis好,對增刪改查的對象的維護要方便。

Hibernate數(shù)據庫移植性很好,MyBatis的數(shù)據庫移植性不好,不同的數(shù)據庫需要寫不同SQL。

Hibernate有更好的二級緩存機制,可以使用第三方緩存。MyBatis本身提供的緩存機制不佳,更新操作不能指定刷新指定記錄,會清空整個表,但是也可以使用第三方緩存。

Hibernate 封裝性好,屏蔽了數(shù)據庫差異,自動生成SQL語句,應對數(shù)據庫變化能力較弱,SQL語句優(yōu)化困難。

MyBatis僅實現(xiàn)了SQL語句和對象的映射,需要針對具體的數(shù)據庫寫SQL語句,應對數(shù)據庫變化能力較強,SQL語句優(yōu)化較為方便。

1.14. SSH 和 SSM 對比總結

SSH 和 SSM 的技術框架的不同只需要比較Struts2和SpringMVC的不同,以及Hibernate和MyBatis的不同。

對于不同的功能,兩大技術陣營均有對應的解決方案。SSH將配置文件開發(fā)用到極致。SSM將注解開發(fā)用到極致。

企業(yè)進行技術選型,以低成本高回報作為技術選型的原則,根據項目組的技術力量來進行選擇。

小弟水平有限,只能總結到這里。更進一步的底層代碼級別的對比,才是本質的區(qū)別。用法上的區(qū)別只是表象而已,但是對于廣大開發(fā)者來說,誰的開發(fā)者用戶體驗好,顯然更能贏得開發(fā)者的青睞。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容