一點一滴學習Spring(七)之Spring MVC的高階技術
Servlet 3.0提供了既能在容器中動態註冊servlet的方法,也提供了通過實現ServletContainerInitializer介面的方法實現在容器啟動階段為容器動態註冊Servlet、Filter和listeners。
容器會在應用的啟動階段,呼叫所有實現ServletContainerInitializer介面類中的onStartup()方法。
而Spring 3.2中,則進一步簡化了這點,只需要實現WebApplicationInitializer介面就可以了,檢視這個介面的原始碼,裡面也非常簡單,只有一個方法,傳入的引數是ServletContext
public interface WebApplicationInitializer
{
public abstract void onStartup(ServletContext servletcontext)
throws ServletException;
}
spring提供了相關的實現類->
AbstractAnnotationConfigDispatcherServletInitializer、AbstractDispatcherServletInitializer
AbstractContextLoaderInitializer可以動態註冊DispatcherServlet。
一、自定義DispatcherServlet配置
通過下面的spring的實現類AbstractAnnotationConfigDispatcherServletInitializer相關原始碼:
public void onStartup(ServletContext servletContext) throws ServletException
{
registerContextLoaderListener(servletContext);
}
protected void registerDispatcherServlet(ServletContext servletContext)
{
String servletName = getServletName();
Assert.hasLength(servletName, "getServletName() may not return empty or null");
WebApplicationContext servletAppContext = createServletApplicationContext();
Assert.notNull(servletAppContext, (new StringBuilder()).append("createServletApplicationContext() did not return an application context for servlet [").append(servletName).append("]").toString());
DispatcherServlet dispatcherServlet = new DispatcherServlet(servletAppContext);
javax.servlet.ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
Assert.notNull(registration, (new StringBuilder()).append("Failed to register servlet with name '").append(servletName).append("'.").append("Check if there is another servlet registered under the same name.").toString());
registration.setLoadOnStartup(1);
registration.addMapping(getServletMappings());
registration.setAsyncSupported(isAsyncSupported());
Filter filters[] = getServletFilters();
if(!ObjectUtils.isEmpty(filters))
{
Filter afilter[] = filters;
int i = afilter.length;
for(int j = 0; j < i; j++)
{
Filter filter = afilter[j];
registerServletFilter(servletContext, filter);
}
}
customizeRegistration(registration);
}
我們可以知道AbstractAnnotationConfigDispatcherServletInitializer將DispatcherServlet註冊到Servlet容器中之後,會呼叫customizeRegistration()方法,並將Servlet註冊後得到的Dynamic registration傳遞進來。所以通過customizeRegistration()方法的重寫我們可以對DispatcherServlet進行額外的配置。如下程式碼所示:
public class SpitterWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{
@Override
protected Class<?>[] getRootConfigClasses() {
// TODO Auto-generated method stub
return new Class<?>[]{RootConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
// TODO Auto-generated method stub
return new Class<?>[]{WebConfig.class};
}
@Override
protected String[] getServletMappings() {
// TODO Auto-generated method stub
return new String[] {"/web/*"};
}
//重寫customizeRegistration方法,實現DispatcherServlet的額外配置
@Override
protected void customizeRegistration(javax.servlet.ServletRegistration.Dynamic dynamic){
}
}
藉助customizeRegistration()方法中的javax.servlet.ServletRegistration.Dynamic dynamic,我們能完成多項任務。
包括通過呼叫setLoadOnStartup()設定load-on-start的優先順序,通過setInitOarameter()設定初始化引數。通過setMultipartConfig()配置Servlet3.0對multipart的支援。
二、新增其他的Servlet和Filter
按照AbstractAnnotationConfigDispatcherServletInitializer的定義,他會建立DispatcherServlet和ContextLoaderListener。
但是,如果你想註冊其他的servlet、filter或Listen的話,那該怎麼辦呢?
基於java的初始化容器(initializer)的一個好處就在於,我們可以定義任意數量的初始化類。因此,我們想往web容器中註冊其他元件的話,只需建立一個新的初始化容器
就可以了。最簡單的方法就是實現Spring的WebApplicationInitializer介面
下面例項->新增一個過濾器:
過濾器配置:
public class FilterConfig implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletcontext) throws ServletException {
// TODO Auto-generated method stub
Dynamic filter = servletcontext.addFilter("myFilter",CustomerFilter.class);
filter.addMappingForUrlPatterns(null, false, "/web/*");
}
}
這裡addMappingForUrlPatterns(EnumSet dispatcherTypes, boolean isMatchAfter, String urlPatterns[])方法,urlPatterns對映需要執行過濾的路徑
CustomerFilter類
public class CustomerFilter implements Filter{
@Override
public void init(FilterConfig filterconfig) throws ServletException {
// TODO Auto-generated method stub
System.out.println("CustomerFilter init...");
}
@Override
public void doFilter(ServletRequest servletrequest, ServletResponse servletresponse, FilterChain filterchain)
throws IOException, ServletException {
// TODO Auto-generated method stub
System.out.println("測試過濾器.....");
filterchain.doFilter(servletrequest, servletresponse);
}
@Override
public void destroy() {
// TODO Auto-generated method stub
System.out.println("CustomerFilter destroy...");
}
}
這個類需要實現Filter
同理:可新增servlet和listen,只要類實現WebApplicationInitializer,並重寫其中的onStartup方法
如果要將應用部署到支援Servlet3.0的容器中,那麼WebApplicationInitializer提供了一種通用的方式,是現在JAVA中註冊Servlet、Filter和Listener。不過,如果你只是註冊Filter,並且該Filter只會對映到DispatcherServlet上的話,那麼在AbstractAnnotationConfigDispatcherServletInitializer中還有一種快捷方式。
為了註冊Filter,並將其對映到DispatcherServlet,所需要做的僅僅是重寫AbstractAnnotationConfigDispatcherServletInitializer的getServletFilters()方法。如下所示:
@Override
protected Filter[] getServletFilters() {
// TODO Auto-generated method stub
return new Filter[]{new CustomerFilter()};
}
getServletFilters方法返回的所有Filter都會對映到DispatcherServlet上。
三、在web.xml中宣告DispatcherServlet
在典型的Spring MVC應用中,我們會需要DispatcherServlet和ContextLoaderListener。AbstractAnnotationConfigDispatcherServletInitializer會自動
註冊它們。但是如果需要在web.xml中註冊的話,那就需要我們自己註冊。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>canal</display-name>
<!--設定上下文配置檔案的位置:檔案會被ContextLoaderListener載入-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath*:/applicationContext-*.xml
</param-value>
</context-param>
<!-- 設定字元過濾器非必須-->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<!-- 指定字元過濾器對映路徑非必須 -->
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Spring MVC前端處理器,註冊DispatcherServlet -->
<servlet>
<servlet-name>Dispatcher Servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--檔案beans.xml會被DispatcherServlet載入-->
<init-param>
<description>Spring MVC定義Bean檔案,該檔案為空配置,所有配置交給上級WebApplicationContext來處理</description>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/beans.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!--DispatcherServlet對映路徑,所有以.html結尾的路徑 -->
<servlet-mapping>
<servlet-name>Dispatcher Servlet</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<!--註冊ContextLoaderListener-->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!--配置其他過濾器及servlet-->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/web/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>CXFService</servlet-name>
<servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>CXFService</servlet-name>
<url-pattern>/web/*</url-pattern>
</servlet-mapping>
</web-app>
ContextLoaderListener和DispatcherServlet各自都會載入一個Spring應用上下文。上下文contextConfigLocation指定了xml檔案的位置。
要在Spring MVC中使用基於Java的配置,我們需要告訴DispatcherServlet和ContextLoaderListener使用AnnotationConfigWebApplicationContext,
這是WebApplicationContext的一個實現類。他會載入Java配置類,而不是使用xml。要實現這種配置,我們可以設定contextClass上下文引數以及
DispatcherServlet的初始化引數。
如下所示,新的web.xml基於java配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<!-- 指定使用Java配置 -->
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.cn.test.config.RootConfig</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!-- Spring MVC前端處理器,註冊DispatcherServlet -->
<servlet>
<servlet-name>Dispatcher Servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--檔案beans.xml會被DispatcherServlet載入-->
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.cn.test.config.WebConfig</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!--DispatcherServlet對映路徑,所有以.html結尾的路徑 -->
<servlet-mapping>
<servlet-name>Dispatcher Servlet</servlet-name>
<url-pattern>/web/*</url-pattern>
</servlet-mapping>
<!--配置其他過濾器及servlet-->
<filter>
<filter-name>customerFilter</filter-name>
<filter-class>com.cn.test.filter.CustomerFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>customerFilter</filter-name>
<url-pattern>/web/*</url-pattern>
</filter-mapping>
</web-app>
四、配置multipart解析器
DispatcherServlet並沒有實現任何解析mulipart請求資料的功能,他將該任務委託給了Spring中的MultipartResolver策略介面的實現。
通過這個實現類來解析multipart請求的內容。從3.1開始,Spring內建類兩個MultipartResolver的實現供我們選擇:
1、CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart請求;
2、StandardServletMultipartResolver依賴於Servlet3.0對multipart請求的支援。始於Spring3.1(優選方案)
使用Servlet3.0解析multipart請求
相容Servlet3.0的StandardServletMultipartResolver沒有任何構造引數,也沒有要設定的屬性。這樣,在Spring應用的上下文中,將其
宣告為bean就會變得非常簡單,如下所示:
@Bean
public MultipartResolver multipartResolver(){
return new StandardServletMultipartResolver();
}
既然@Bean方法如此簡單,那麼我們該如何限制StandardServletMultipartResolver的工作方式呢?怎麼設定上傳檔案的大小及臨時儲存目錄呢?
對於沒有建構函式和設定屬性的StandardServletMultipartResolver來說,這似乎是很難限制的。
其實並不是這樣的,我們是有辦法設定StandardServletMultipartResolver的限制條件的,只不過不在Spring中配置StandardServletMultipartResolver,而只要在Servlet中指定multipart的配置。還記得我們前面所說的customizeRegistration()方法嗎?下面就用上了此方法:
//過載customizeRegistration方法,實現DispatcherServlet的額外配置
@Override
protected void customizeRegistration(javax.servlet.ServletRegistration.Dynamic dynamic){
//"/tmp/uploads"為臨時儲存路徑
MultipartConfigElement configElement = new MultipartConfigElement("/tmp/uploads");
dynamic.setMultipartConfig(configElement);
}
整個檔案:
public class SpitterWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{
@Override
protected Class<?>[] getRootConfigClasses() {
// TODO Auto-generated method stub
return new Class<?>[]{RootConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
// TODO Auto-generated method stub
return new Class<?>[]{WebConfig.class};
}
@Override
protected String[] getServletMappings() {
// TODO Auto-generated method stub
return new String[] {"/web/*"};
}
//過載customizeRegistration方法,實現DispatcherServlet的額外配置
@Override
protected void customizeRegistration(javax.servlet.ServletRegistration.Dynamic dynamic){
//"/tmp/uploads"為臨時儲存路徑--強制設定
MultipartConfigElement configElement = new MultipartConfigElement("/tmp/uploads");
dynamic.setMultipartConfig(configElement);
}
@Override
protected Filter[] getServletFilters() {
// TODO Auto-generated method stub
return new Filter[]{new CustomerFilter()};
}
}
使用web.xml配置的程式碼片段
<servlet>
<servlet-name>Dispatcher Servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--檔案beans.xml會被DispatcherServlet載入-->
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.cn.test.config.WebConfig</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<multipart-config>
<location>/tmp/uploads</location>
<file-size-threshold>0</file-size-threshold>
<max-file-size>2097152</max-file-size><!-- 2M -->
<max-request-size>4194304</max-request-size><!-- 4M -->
</multipart-config>
</servlet>
<!--DispatcherServlet對映路徑,所有以.html結尾的路徑 -->
<servlet-mapping>
<servlet-name>Dispatcher Servlet</servlet-name>
<url-pattern>/web/*</url-pattern>
</servlet-mapping>
配置Jakarta Commons FileUpload multipart解析器
@Bean
public MultipartResolver multipartResolver() throws IOException{
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
resolver.setUploadTempDir(new FileSystemResource("臨時檔案路徑"));//,非必須設定
resolver.setMaxInMemorySize(4096);//最大記憶體大小
resolver.setMaxUploadSize(2097152);//上傳檔案大小限制
return resolver;
}
xml檔案設定
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="2097152" />
<property name="maxInMemorySize" value="4096" />
</bean>
處理multipart請求
在controller方法的接收引數上新增@RequestPart(“file”) byte[] file
例如:
@RequestMapping(value="/home/file",method=RequestMethod.POST)//處理對/web/home/test的請求
public String home_file(Model model,@RequestPart("file") byte[] file){
}
若是提交表單時,沒有選擇圖片,那麼這個陣列是空,而非null。那麼我們要如何將byte陣列轉化為儲存檔案那,看下一部分
接收MultipartFile
public String home_file(Model model,@RequestPart("file") MultipartFile file){
}
五、處理異常
專案中實際用到的統一捕獲異常方式:
1、Spring boot中使用的異常捕獲
@Component
@ControllerAdvice
public class GlobalDefaultExceptionHandler{
private Logger exception = LoggerFactory.getLogger("exception");
//新增全域性異常處理流程,根據需要設定需要處理的異常
@ExceptionHandler(value=Exception.class)
@ResponseBody
public MsgHeader defaultErrorHandler(HttpServletRequest request,
Exception e) throws Exception
{
//按需重新封裝需要返回的錯誤資訊
//此處列印錯誤日誌
e.printStackTrace();
return new MsgHeader(CodeEnum.EXCEPTION.getCode(),CodeEnum.EXCEPTION.getDesc_enu());
}
//新增全域性異常處理流程,捕獲客戶端自己的異常
@ExceptionHandler(value={ServiceException.class})
@ResponseBody
public MsgHeader jsonErrorHandler(HttpServletRequest request,
ServiceException e) throws Exception
{
//此處列印錯誤日誌
exception.error(e.getCode()+"---"+e.getDesc());
return new MsgHeader(e.getCode(), e.getDesc());
}
//新增全域性異常處理流程,捕獲服務層丟擲的自定義異常
@ExceptionHandler(value={BaseException.class})
@ResponseBody
public MsgHeader jsonErrorHandler(HttpServletRequest request,
BaseException e) throws Exception
{
//此處列印錯誤日誌
exception.error(e.getCode()+"---"+e.getMsg());
return new MsgHeader(e.getCode(), e.getMsg());
}
}
@ExceptionHandler標註的方法處理給定的異常
類級別使用@ControllerAdvice註解:標明他是一個控制器通知
@ResponseBody返回json格式資料
這個類一定要配置在spring能夠掃描到的位置
2、Spring中統一捕獲異常
public class ExceptionAdvisor implements ThrowsAdvice{
private static final Logger log = LoggerFactory.getLogger(ExceptionAdvisor.class);
public void afterThrowing(Method method, Object[] args, Object target,
Exception ex) throws Throwable
{
// 在後臺中輸出錯誤異常異常資訊,通過log4j輸出。
log.info("**************************************************************");
log.info("Error happened in class: " + target.getClass().getName());
log.info("Error happened in method: " + method.getName());
for (int i = 0; i < args.length; i++)
{
log.info("arg[" + i + "]: " + args[i]);
}
log.info("Exception class: " + ex.getClass().getName());
log.info("ex.getMessage():" + ex.getMessage());
log.info("**************************************************************");
// 在這裡判斷異常,根據不同的異常返回錯誤。
if (ex.getClass().equals(ConstraintViolationException.class)){
ex.printStackTrace();
ConstraintViolationException exc = (ConstraintViolationException) ex;
String enumName = exc.getConstraintViolations().iterator().next().getMessage();
log.info("enumName--------"+enumName);
CodeEnum enumCode;
try {
enumCode = CodeEnum.valueOf(enumName);
} catch (Exception e) {
//若是名稱不能成功轉化為列舉,則給定common列舉
enumCode = CodeEnum.VALIDATE_COMMON;
}
log.info("code:"+enumCode.getCode()+"--enu desc--"+enumCode.getDesc_enu());
throw new BaseException(enumCode.getCode(), enumCode.getDesc_enu());
}else{
ex.printStackTrace();
throw ex;
}
}
}
springContext.xml中新增配置
<bean id="exceptionHandler" class="com.isgo.gallerydao.core.exception.ExceptionAdvisor"></bean>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" >
<property name="beanNames">
<list> <!-- 配置需要進行日誌記錄的Service和Dao -->
<value>*Service</value> <!-- Service層的Bean ID 命名要以Service結尾 -->
</list>
</property>
<property name="interceptorNames">
<list>
<value>exceptionHandler</value>
</list>
</property>
</bean>
3、CXF配置統一捕獲異常:
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.ext.ExceptionMapper;
public class InvokeFaultExceptionMapper implements ExceptionMapper {
private static Logger logger = LogManager.getLogger("exception");
@Override
public Response toResponse(Throwable ex) {
StackTraceElement[] trace = new StackTraceElement[1];
trace[0] = ex.getStackTrace()[0];
ex.setStackTrace(trace);
ResponseBuilder rb = Response.status(Response.Status.OK);
rb.type("application/json;charset=UTF-8");
if (ex instanceof ServiceException) {//自定義的異常類
ServiceException e = (ServiceException) ex;
ServiceExceptionEntity entity = new ServiceExceptionEntity(e.getCode(),e.getDesc());
rb.entity(entity);
}else{
ServiceExceptionEntity entity = new ServiceExceptionEntity(CodeEnum.EXCEPTION.getCode(),
CodeEnum.EXCEPTION.getDesc_enu());
rb.entity(entity);
}
if(null!=trace[0]){
logger.error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
logger.error("className:{},fileName:{},methodName:{},lineNumber:{},cause:{}.",trace[0].getClassName(),
trace[0].getFileName(),trace[0].getMethodName(),trace[0].getLineNumber(),ex);
}
rb.language(Locale.SIMPLIFIED_CHINESE);
Response r = rb.build();
return r;
}
}
ExceptionMapper在包:javax.ws.rs-api.jar中
<bean id="invokeFaultExceptionMapper" class="com.canal.api.exception.InvokeFaultExceptionMapper"/>
六、@ControllerAdvice註解
控制類通知,這個類會包含一個或多個如下型別的方法:
1、@ExceptionHandler註解標註的方法–使所有的控制類異常在一個地方統一處理。參考Spring boot中使用的異常捕獲
2、@InitBinder註解標註的方法
3、@ModelAttribute註解標註的方法