Java-Web|Tomcat8.5.23 ApplicationFilterChain源码分析(职责链模式)
一、前言
因为在前一篇文章中分析JavaWeb项目切入点时提到了一种从Filter入手的思路,所以本文来探究一下多个Filter之间究竟是怎么组织、运行的。
二、正文
1. 源码的运行流程分析 (1)ApplicationFilterChain对象的特点与创建
特点:经过一路代码跟踪发现,每一个url匹配模式对应于一个ApplicationFilterChain对象,在应用的整个生命周期中只在第一次被访问时被创建一次,有种单例模式的感觉,但是并没有做线程安全方面的处理,后来发现可能做了池化处理;
创建:ApplicationFilterChain对象是在StandardWrapperValve类中的invoke方法中调用ApplicationFilterFactory.createFilterChain方法创建的,在实例化完成后紧接着就是为其设置Servlet,添加Filters以及设置其他属性,添加Filters依赖于一个FilterMap[]数组,该数组的赋值与扩容过程在StandardContext类中实现,至于FilterMap的生成,则是在Spring的Bean初始化阶段,通过扫描包下所有的类并结合注解通过反射机制进行实例化的。
核心的ApplicationFilterFactory类代码如下:
//ApplicationFilterFactory.java
/**
* Construct a FilterChain implementation that will wrap the execution of
* the specified servlet instance.
*
* @param request The servlet request we are processing
* @param wrapper The wrapper managing the servlet instance
* @param servlet The servlet instance to be wrapped
*
* @return The configured FilterChain instance or null if none is to be
*executed.
*/
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {// If there is no servlet to execute, return null
if (servlet == null)
return null;
// Create and initialize a filter chain object
ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request) request;
if (Globals.IS_SECURITY_ENABLED) {
// Security: Do not recycle
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain) req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
// Request dispatcher in use
filterChain = new ApplicationFilterChain();
}filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
// If there are no filter mappings, we are done
if ((filterMaps == null) || (filterMaps.length == 0))
return (filterChain);
// Acquire the information we will need to match filter mappings
DispatcherType dispatcher =
(DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);
String requestPath = null;
Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
if (attribute != null){
requestPath = attribute.toString();
}String servletName = wrapper.getName();
// Add the relevant path-mapped filters to this filter chain
for (int i = 0;
i < filterMaps.length;
i++) {
if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMaps[i], requestPath))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}// Add filters that match on servlet name second
for (int i = 0;
i < filterMaps.length;
i++) {
if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMaps[i], servletName))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}// Return the completed filter chain
return filterChain;
}
(2)ApplicationFilterChain对象的调用
filterChain的调用是在StandardWrapperValve类的invoke方法中进行的,这里也把其代码贴出来:
//StandardWrapperValve.java
/**
* Invoke the servlet we are managing, respecting the rules regarding
* servlet lifecycle and SingleThreadModel support.
*
* @param request Request to be processed
* @param response Response to be produced
*
* @exception IOException if an input/output error occurred
* @exception ServletException if a servlet error occurred
*/
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {// Initialize local variables we may need
boolean unavailable = false;
Throwable throwable = null;
// This should be a Request attribute...
long t1=System.currentTimeMillis();
requestCount.incrementAndGet();
StandardWrapper wrapper = (StandardWrapper) getContainer();
Servlet servlet = null;
Context context = (Context) wrapper.getParent();
// Check for the application being marked unavailable
if (!context.getState().isAvailable()) {
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardContext.isUnavailable"));
unavailable = true;
}// Check for the servlet being marked unavailable
if (!unavailable && wrapper.isUnavailable()) {
container.getLogger().info(sm.getString("standardWrapper.isUnavailable",
wrapper.getName()));
long available = wrapper.getAvailable();
if ((available > 0L) && (available < Long.MAX_VALUE)) {
response.setDateHeader("Retry-After", available);
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardWrapper.isUnavailable",
wrapper.getName()));
} else if (available == Long.MAX_VALUE) {
response.sendError(HttpServletResponse.SC_NOT_FOUND,
sm.getString("standardWrapper.notFound",
wrapper.getName()));
}
unavailable = true;
}// Allocate a servlet instance to process this request
try {
if (!unavailable) {
servlet = wrapper.allocate();
}
} catch (UnavailableException e) {
container.getLogger().error(
sm.getString("standardWrapper.allocateException",
wrapper.getName()), e);
long available = wrapper.getAvailable();
if ((available > 0L) && (available < Long.MAX_VALUE)) {
response.setDateHeader("Retry-After", available);
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardWrapper.isUnavailable",
wrapper.getName()));
} else if (available == Long.MAX_VALUE) {
response.sendError(HttpServletResponse.SC_NOT_FOUND,
sm.getString("standardWrapper.notFound",
wrapper.getName()));
}
} catch (ServletException e) {
container.getLogger().error(sm.getString("standardWrapper.allocateException",
wrapper.getName()), StandardWrapper.getRootCause(e));
throwable = e;
exception(request, response, e);
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.allocateException",
wrapper.getName()), e);
throwable = e;
exception(request, response, e);
servlet = null;
}MessageBytes requestPathMB = request.getRequestPathMB();
DispatcherType dispatcherType = DispatcherType.REQUEST;
if (request.getDispatcherType()==DispatcherType.ASYNC) dispatcherType = DispatcherType.ASYNC;
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,dispatcherType);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
requestPathMB);
// Create the filter chain for this request
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
// Call the filter chain for this request
// NOTE: This also calls the servlet's service() method
try {
if ((servlet != null) && (filterChain != null)) {
// Swallow output if needed
if (context.getSwallowOutput()) {
try {
SystemLogHandler.startCapture();
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter(request.getRequest(),
response.getResponse());
}
} finally {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
context.getLogger().info(log);
}
}
} else {
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter
(request.getRequest(), response.getResponse());
}
}}
} catch (ClientAbortException e) {
throwable = e;
exception(request, response, e);
} catch (IOException e) {
container.getLogger().error(sm.getString(
"standardWrapper.serviceException", wrapper.getName(),
context.getName()), e);
throwable = e;
exception(request, response, e);
} catch (UnavailableException e) {
container.getLogger().error(sm.getString(
"standardWrapper.serviceException", wrapper.getName(),
context.getName()), e);
//throwable = e;
//exception(request, response, e);
wrapper.unavailable(e);
long available = wrapper.getAvailable();
if ((available > 0L) && (available < Long.MAX_VALUE)) {
response.setDateHeader("Retry-After", available);
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardWrapper.isUnavailable",
wrapper.getName()));
} else if (available == Long.MAX_VALUE) {
response.sendError(HttpServletResponse.SC_NOT_FOUND,
sm.getString("standardWrapper.notFound",
wrapper.getName()));
}
// Do not save exception in 'throwable', because we
// do not want to do exception(request, response, e) processing
} catch (ServletException e) {
Throwable rootCause = StandardWrapper.getRootCause(e);
if (!(rootCause instanceof ClientAbortException)) {
container.getLogger().error(sm.getString(
"standardWrapper.serviceExceptionRoot",
wrapper.getName(), context.getName(), e.getMessage()),
rootCause);
}
throwable = e;
exception(request, response, e);
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString(
"standardWrapper.serviceException", wrapper.getName(),
context.getName()), e);
throwable = e;
exception(request, response, e);
}// Release the filter chain (if any) for this request
if (filterChain != null) {
filterChain.release();
}// Deallocate the allocated servlet instance
try {
if (servlet != null) {
wrapper.deallocate(servlet);
}
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.deallocateException",
wrapper.getName()), e);
if (throwable == null) {
throwable = e;
exception(request, response, e);
}
}// If this servlet has been marked permanently unavailable,
// unload it and release this instance
try {
if ((servlet != null) &&
(wrapper.getAvailable() == Long.MAX_VALUE)) {
wrapper.unload();
}
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.unloadException",
wrapper.getName()), e);
if (throwable == null) {
throwable = e;
exception(request, response, e);
}
}
long t2=System.currentTimeMillis();
long time=t2-t1;
processingTime += time;
if( time > maxTime) maxTime=time;
if( time < minTime) minTime=time;
}
(3)ApplicationFilterChain对象的内部运行逻辑分析
ApplicationFilterChain的设计采用了“职责链”的设计模式,我对职责链的理解是它与递归调用是类似的,区别在于递归调用是同一个对象把子任务交给同一个方法本身去完成,而职责链则是一个对象把子任务交给其他对象的其他方法去完成,二者均可在调用堆栈的中间直接返回,也可等最深层的方法执行完逐层返回,其核心在于上下文(ApplicationFilterChain对象)在不同对象(Filter)间的传递与状态(pos)的改变,针对ApplicationFilterChain,其示意图如下:
文章图片
该类中最重要的三个方法分别是addFilter、doFilter与internalDoFilter,在注释中直接进行分析如下:
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License");
you may not use this file except in compliance with
* the License.You may obtain a copy of the License at
*
*http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina.core;
import java.io.IOException;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.Globals;
import org.apache.catalina.security.SecurityUtil;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.res.StringManager;
/**
* Implementation of javax.servlet.FilterChain
used to manage
* the execution of a set of filters for a particular request.When the
* set of defined filters has all been executed, the next call to
* doFilter()
will execute the servlet's service()
* method itself.
*
* @author Craig R. McClanahan
*/
public final class ApplicationFilterChain implements FilterChain {// Used to enforce requirements of SRV.8.2 / SRV.14.2.5.1
private static final ThreadLocal> lastServicedRequest;
private static final ThreadLocal> lastServicedResponse;
static {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest = new ThreadLocal<>();
lastServicedResponse = new ThreadLocal<>();
} else {
lastServicedRequest = null;
lastServicedResponse = null;
}
}// -------------------------------------------------------------- Constants //filters数组每次扩容增量
public static final int INCREMENT = 10;
// ----------------------------------------------------- Instance Variables/**
* Filters.
*/
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
/**
* The int which is used to maintain the current position
* in the filter chain.
*/
//当前执行的filter下标
private int pos = 0;
/**
* The int which gives the current number of filters in the chain.
*/
//filter数量
private int n = 0;
/**
* The servlet instance to be executed by this chain.
*/
//filter终端的Servlet
private Servlet servlet = null;
/**
* Does the associated servlet instance support async processing?
*/
private boolean servletSupportsAsync = false;
/**
* The string manager for our package.
*/
private static final StringManager sm =
StringManager.getManager(Constants.Package);
/**
* Static class array used when the SecurityManager is turned on and
* doFilter
is invoked.
*/
private static final Class>[] classType = new Class[]{
ServletRequest.class, ServletResponse.class, FilterChain.class};
/**
* Static class array used when the SecurityManager is turned on and
* service
is invoked.
*/
private static final Class>[] classTypeUsedInService = new Class[]{
ServletRequest.class, ServletResponse.class};
// ---------------------------------------------------- FilterChain Methods/**
* Invoke the next filter in this chain, passing the specified request
* and response.If there are no more filters in this chain, invoke
* the service()
method of the servlet itself.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
*
* @exception IOException if an input/output error occurs
* @exception ServletException if a servlet exception occurs
*/
//主要进行一层安全验证处理,内部调用internalDoFilter
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
try {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction() {
@Override
public Void run()
throws ServletException, IOException {
internalDoFilter(req,res);
return null;
}
}
);
} catch( PrivilegedActionException pe) {
Exception e = pe.getException();
if (e instanceof ServletException)
throw (ServletException) e;
else if (e instanceof IOException)
throw (IOException) e;
else if (e instanceof RuntimeException)
throw (RuntimeException) e;
else
throw new ServletException(e.getMessage(), e);
}
} else {
internalDoFilter(request,response);
}
} //实际的Filter方法
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {// Call the next filter if there is one
//如果filter没有执行完毕,且filter中一直保持了filterChain的链式调用
if (pos < n) {
//根据pos定位找到ApplicationFilterConfig
ApplicationFilterConfig filterConfig = filters[pos++];
try {
//拆包取得Filter对象
Filter filter = filterConfig.getFilter();
//异步判断
if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
//安全判断,特殊处理
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
} else {
//调用用户编写的Filter中的方法进行过滤
filter.doFilter(request, response, this);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
//这里非常关键,之前的版本是if-else写法,这里改为了在if中return
return;
}// We fell off the end of the chain -- call the servlet instance
//执行Servlet的service方法
//只有filters全部执行完毕后才能执行到这里,若在某个Filter中提前结束链式调用,则Servlet不会执行
try {
//保存Servlet执行前的request与response
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}if (request.isAsyncSupported() && !servletSupportsAsync) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}
// Use potentially wrapped request from this point
//安全判断、特殊处理
if ((request instanceof HttpServletRequest) &&
(response instanceof HttpServletResponse) &&
Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res};
SecurityUtil.doAsPrivilege("service",
servlet,
classTypeUsedInService,
args,
principal);
} else {
//Servlet调用
servlet.service(request, response);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.servlet"), e);
} finally {
//暂时还不明白这两个对象有什么用
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(null);
lastServicedResponse.set(null);
}
}
}/**
* The last request passed to a servlet for servicing from the current
* thread.
*
* @return The last request to be serviced.
*/
public static ServletRequest getLastServicedRequest() {
return lastServicedRequest.get();
}/**
* The last response passed to a servlet for servicing from the current
* thread.
*
* @return The last response to be serviced.
*/
public static ServletResponse getLastServicedResponse() {
return lastServicedResponse.get();
}// -------------------------------------------------------- Package Methods/**
* Add a filter to the set of filters that will be executed in this chain.
*
* @param filterConfig The FilterConfig for the servlet to be executed
*/
void addFilter(ApplicationFilterConfig filterConfig) {// Prevent the same filter being added multiple times
//防止添加重复的Filter,将Filter执行多次
for(ApplicationFilterConfig filter:filters)
if(filter==filterConfig)
return;
//若filters数组容量已满
if (n == filters.length) {
//扩容:每次增加INCREMENT
ApplicationFilterConfig[] newFilters =
new ApplicationFilterConfig[n + INCREMENT];
//数组内容拷贝
System.arraycopy(filters, 0, newFilters, 0, n);
//引用替换
filters = newFilters;
}
//添加新的FilterConfig对象
filters[n++] = filterConfig;
}/**
* Release references to the filters and wrapper executed by this chain.
*/
//该方法应该是用于filterChain池中的不同URL-Pattern对应的filterChain对象复用,节省对象创建开销
void release() {
for (int i = 0;
i < n;
i++) {
filters[i] = null;
}
n = 0;
pos = 0;
servlet = null;
servletSupportsAsync = false;
}/**
* Prepare for reuse of the filters and wrapper executed by this chain.
*/
//该方法应该是用于filterChain池中的相同URL-Pattern对应的filterChain对象复用,复用成本更低
void reuse() {
pos = 0;
}/**
* Set the servlet that will be executed at the end of this chain.
*
* @param servlet The Wrapper for the servlet to be executed
*/
void setServlet(Servlet servlet) {
this.servlet = servlet;
}void setServletSupportsAsync(boolean servletSupportsAsync) {
this.servletSupportsAsync = servletSupportsAsync;
}/**
* Identifies the Filters, if any, in this FilterChain that do not support
* async.
*
* @param result The Set to which the fully qualified class names of each
*Filter in this FilterChain that does not support async will
*be added
*/
//查找非异步的Filters
public void findNonAsyncFilters(Set> result) {
for (int i = 0;
i < n ;
i++) {
ApplicationFilterConfig filter = filters[i];
if ("false".equalsIgnoreCase(filter.getFilterDef().getAsyncSupported())) {
result.add(filter.getFilterClass());
}
}
}
}
2. 分析源码时的额外收获 (1)所有配置了路由信息的处理方法最终都是通过反射的方式进行调用的;
(2)在Java8中,反射方法调用最终落脚于NativeMethodAccessorImpl类的native方法:
private static native Object invoke0(Method var0, Object var1, Object[] var2);
在此处与JVM底层交互,实现跨代码衔接执行;
(3)观察到的比较重要的设计模式:职责链模式(ApplicationFilterChain)、委派模式(DelegatingFilterProxy)、工厂模式、策略模式、代理模式(FilterChainProxy)、外观模式、适配器模式(HandlerAdapter);
(4)Tomcat与SpringMVC的结合点:ApplicationFilterChain与DispatcherServlet(继承于FrameworkServlet);
(5)在集成了Tomcat的SpringBoot项目中,先启动的不是Tomcat,而是Spring,Spring的工厂(默认DefaultListableBeanFactory)读取注解完成各类Bean(WebApplicationContext、securityFilterChainRegistration、dispatcherServletRegistration、各类FilterInitializer与Filter)的初始化,放入IoC容器,然后做路由Mapping,创建FilterChain,开启JMX等;
(6)Servlet、Filter是单实例多线程的,成员变量线程不安全,方法内局部变量线程安全;SingleThreadModel采用同步/实例池的方式来确保不会有两个线程同时执行servlet的service方法,但已被弃用,需自行确保成员变量线程安全;
3. 还存在的问题 【Java-Web|Tomcat8.5.23 ApplicationFilterChain源码分析(职责链模式)】(1)既然ApplicationFilterChain对于每一个Url Pattern是单例,那么其成员变量(例如pos和n)如何保证线程安全?
调试中观察到的现象:“ApplicationFilterChain对于每一个Url Pattern是单例”是错误的,打上断点后的两个并发请求会调用两个不同的filterChain对象来处理,但非并发的时候始终是同一个,猜测filterChain应该也是有一个池进行管理,确保不会有两个线程同时执行doFilter方法造成冲突,但是这样一来类中的两个ThreadLocal型成员变量就有点看不懂了,细节还有待深究。
三、后记
源码分析部分牵扯到的相关知识很多,一分析起来就得花很长的时间去调试代码,很多时候难以割裂开来单独讨论,一篇博客里面也不可能讲得很多很全,再加上自己水平也有限,所以若文中有错误、遗漏,请各路大佬不吝指出;
项目、源码分析系列将持续更新,欢迎关注。
推荐阅读
- Application|linux应用编程笔记(5)系统调用文件编程方法实现文件复制
- Supported|Supported orientations has no common orientation with the application 解决方案
- java|ApplicationListener和SpringApplicationRunListener的联系
- Android中Application、Activity和Service它们真正干活的Context是什么()
- Android|Application、Activity、Service和Context之间的构建关系
- SpringBoot之@ComponentScan和@SpringBootApplication扫描覆盖问题
- Express|Express 工具库中的 Application 对象
- springboot项目配置application添加图片映射 (windows and linux 都可使用)
- Android 多点触控屏蔽
- Android Manifests Application节点属性全解析