最近在看一些dbcp的相关内容,顺便做一下记录,免得自己给忘记了。
 
1. 引入dbcp (选择1.4)
  1. <dependency>
  2. <groupId>com.alibaba.external</groupId>
  3. <artifactId>jakarta.commons.dbcp</artifactId>
  4. <version>1.4</version>
  5. </dependency>

2. dbcp的基本配置

相关配置说明:

  1. initialSize :连接池启动时创建的初始化连接数量(默认值为0)
  2. maxActive :连接池中可同时连接的最大的连接数(默认值为8,调整为20,高峰单机器在20并发左右,自己根据应用场景定)
  3. maxIdle:连接池中最大的空闲的连接数,超过的空闲连接将被释放,如果设置为负数表示不限制(默认为8个,maxIdle不能设置太小,因为假如在高负载的情况下,连接的打开时间比关闭的时间快,会引起连接池中idle的个数 上升超过maxIdle,而造成频繁的连接销毁和创建,类似于jvm参数中的Xmx设置)
  4. minIdle:连接池中最小的空闲的连接数,低于这个数量会被创建新的连接(默认为0,调整为5,该参数越接近maxIdle,性能越好,因为连接的创建和销毁,都是需要消耗资源的;但是不能太大,因为在机器很空闲的时候,也会创建低于minidle个数的连接,类似于jvm参数中的Xmn设置)
  5. maxWait  :最大等待时间,当没有可用连接时,连接池等待连接释放的最大时间,超过该时间限制会抛出异常,如果设置-1表示无限等待(默认为无限,调整为60000ms,避免因线程池不够用,而导致请求被无限制挂起)
  6. poolPreparedStatements:开启池的prepared(默认是false,未调整,经过测试,开启后的性能没有关闭的好。)
  7. maxOpenPreparedStatements:开启池的prepared 后的同时最大连接数(默认无限制,同上,未配置)
  8. minEvictableIdleTimeMillis  :连接池中连接,在时间段内一直空闲, 被逐出连接池的时间
  9. (默认为30分钟,可以适当做调整,需要和后端服务端的策略配置相关)
  10. removeAbandonedTimeout  :超过时间限制,回收没有用(废弃)的连接(默认为 300秒,调整为180)
  11. removeAbandoned  :超过removeAbandonedTimeout时间后,是否进 行没用连接(废弃)的回收(默认为false,调整为true)
 
removeAbandoned参数解释:
  1. 如果开启了removeAbandoned,当getNumIdle() < 2) and (getNumActive() > getMaxActive() - 3)时被触发.
  2. 举例当maxActive=20, 活动连接为18,空闲连接为1时可以触发"removeAbandoned".但是活动连接只有在没有被使用的时间超 过"removeAbandonedTimeout"时才被回收
  3. logAbandoned: 标记当连接被回收时是否打印程序的stack traces日志(默认为false,未调整)
 
一般会是几种情况出现需要removeAbandoned: 
  1. 代码未在finally释放connection , 不过我们都用sqlmapClientTemplate,底层都有链接释放的过程
  2. 遇到数据库死锁。以前遇到过后端存储过程做了锁表操作,导致前台集群中连接池全都被block住,后续的业务处理因为拿不到链接所有都处理失败了。
 
一份优化过的配置:
  1. <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
  2. <property name="driverClassName" value="com.mysql.jdbc.Driver" />
  3. <property name="url" value="xxxx" />
  4. <property name="username"><value>xxxx</value></property>
  5. <property name="password"><value>xxxxx</value></property>
  6. <property name="maxActive"><value>20</value></property>
  7. <property name="initialSize"><value>1</value></property>
  8. <property name="maxWait"><value>60000</value></property>
  9. <property name="maxIdle"><value>20</value></property>
  10. <property name="minIdle"><value>3</value></property>
  11. <property name="removeAbandoned"><value>true</value></property>
  12. <property name="removeAbandonedTimeout"><value>180</value></property>
  13. <property name="connectionProperties"><value>clientEncoding=GBK</value></property>
  14. </bean>
 
 
2. dbcp的链接validate配置
  1. dbcp是采用了commons-pool做为其连接池管理,testOnBorrow,testOnReturn, testWhileIdle是pool是提供的几种校验机制,通过外部钩子的方式回调dbcp的相关数据库链接(validationQuery)校验
  2. dbcp相关外部钩子类:PoolableConnectionFactory,继承于common-pool PoolableObjectFactory
  3. dbcp通过GenericObjectPool这一入口,进行连接池的borrow,return处理
  4. testOnBorrow : 顾明思义,就是在进行borrowObject进行处理时,对拿到的connection进行validateObject校验
  5. testOnReturn : 顾明思义,就是在进行returnObject对返回的connection进行validateObject校验,个人觉得对数据库连接池的管理意义不大
  6. testWhileIdle : 关注的重点,GenericObjectPool中针对pool管理,起了一个Evict的TimerTask定时线程进行控制(可通过设置参数timeBetweenEvictionRunsMillis>0),定时对线程池中的链接进行validateObject校验,对无效的链接进行关闭后,会调用ensureMinIdle,适当建立链接保证最小的minIdle连接数。
  7. timeBetweenEvictionRunsMillis,设置的Evict线程的时间,单位ms,大于0才会开启evict检查线程
  8. validateQuery, 代表检查的sql
  9. validateQueryTimeout, 代表在执行检查时,通过statement设置,statement.setQueryTimeout(validationQueryTimeout)
  10. numTestsPerEvictionRun,代表每次检查链接的数量,建议设置和maxActive一样大,这样每次可以有效检查所有的链接.
  1. <property name="testWhileIdle"><value>true</value></property> <!-- 打开检查,用异步线程evict进行检查 -->
  2. <property name="testOnBorrow"><value>false</value></property>
  3. <property name="testOnReturn"><value>false</value></property>
  4. <property name="validationQuery"><value>select sysdate from dual</value></property>
  5. <property name="validationQueryTimeout"><value>1</value></property>
  6. <property name="timeBetweenEvictionRunsMillis"><value>30000</value></property>
  7. <property name="numTestsPerEvictionRun"><value>20</value></property>

相关配置需求:

  1. 目前网站的应用大部分的瓶颈还是在I/O这一块,大部分的I/O还是在数据库的这一层面上,每一个请求可能会调用10来次SQL查询,如果不走事务,一个请求会重复获取链接,如果每次获取链接都进行validateObject,性能开销不是很能接受,可以假定一次SQL操作消毫0.5~1ms(一般走了网络请求基本就这数)
  2. 网站异常数据库重启,网络异常断开的频率是非常低的,一般也就在数据库升级,演习维护时才会进行,而且一般也是选在晚上,访问量相对比较低的请求,而且一般会有人员值班关注,所以异步的validateObject是可以接受,但一个前提需要确保能保证在一个合理的时间段内,数据库能完成自动重联。
 
从代码层面简单介绍下dbcp的validate实现:
 
1.  common-pools提供的PoolableObjectFactory,针对pool池的管理操作接口
  1. public interface PoolableObjectFactory {
  2. Object makeObject() throws Exception;
  3. void destroyObject(Object obj) throws Exception;
  4. boolean validateObject(Object obj);
  5. void activateObject(Object obj) throws Exception;
  6. void passivateObject(Object obj) throws Exception;
  7. }

2. dbcp实现的pool从池管理操作

这里贴了一个相关validate代码,具体类可见:PoolableConnectionFactory.validateConnection()

  1. public class PoolableConnectionFactory implements PoolableObjectFactory {
  2. ......
  3. public boolean validateObject(Object obj) { //验证validateObject
  4. if(obj instanceof Connection) {
  5. try {
  6. validateConnection((Connection) obj);
  7. return true;
  8. } catch(Exception e) {
  9. return false;
  10. }
  11. } else {
  12. return false;
  13. }
  14. }
  15. public void validateConnection(Connection conn) throws SQLException {
  16. String query = _validationQuery;
  17. if(conn.isClosed()) {
  18. throw new SQLException("validateConnection: connection closed");
  19. }
  20. if(null != query) {
  21. Statement stmt = null;
  22. ResultSet rset = null;
  23. try {
  24. stmt = conn.createStatement();
  25. if (_validationQueryTimeout > 0) {
  26. stmt.setQueryTimeout(_validationQueryTimeout);
  27. }
  28. rset = stmt.executeQuery(query);
  29. if(!rset.next()) {
  30. throw new SQLException("validationQuery didn't return a row");
  31. }
  32. } finally {
  33. if (rset != null) {
  34. try {
  35. rset.close();
  36. } catch(Exception t) {
  37. // ignored
  38. }
  39. }
  40. if (stmt != null) {
  41. try {
  42. stmt.close();
  43. } catch(Exception t) {
  44. // ignored
  45. }
  46. }
  47. }
  48. }
  49. }
  50. ....
  51. }

3. pool池的evict调用代码:GenericObjectPool (apache commons pool version 1.5.4)

  1. protected synchronized void startEvictor(long delay) { //启动Evictor为TimerTask
  2. if(null != _evictor) {
  3. EvictionTimer.cancel(_evictor);
  4. _evictor = null;
  5. }
  6. if(delay > 0) {
  7. _evictor = new Evictor();
  8. EvictionTimer.schedule(_evictor, delay, delay);
  9. }
  10. }
  11. for (int i=0,m=getNumTests();i<m;i++) {
  12. final ObjectTimestampPair pair;
  13. .......
  14. boolean removeObject = false;
  15. // 空闲链接处理
  16. final long idleTimeMilis = System.currentTimeMillis() - pair.tstamp;
  17. if ((getMinEvictableIdleTimeMillis() > 0) &&
  18. (idleTimeMilis > getMinEvictableIdleTimeMillis())) {
  19. removeObject = true;
  20. } else if ((getSoftMinEvictableIdleTimeMillis() > 0) &&
  21. (idleTimeMilis > getSoftMinEvictableIdleTimeMillis()) &&
  22. ((getNumIdle() + 1)> getMinIdle())) {
  23. removeObject = true;
  24. }
  25. //  testWhileIdle sql 检查处理
  26. if(getTestWhileIdle() && !removeObject) {
  27. boolean active = false;
  28. try {
  29. _factory.activateObject(pair.value);
  30. active = true;
  31. } catch(Exception e) {
  32. removeObject=true;
  33. }
  34. if(active) {
  35. if(!_factory.validateObject(pair.value)) {
  36. removeObject=true;
  37. } else {
  38. try {
  39. _factory.passivateObject(pair.value);
  40. } catch(Exception e) {
  41. removeObject=true;
  42. }
  43. }
  44. }
  45. }
  46. // 真正关闭
  47. if (removeObject) {
  48. try {
  49. _factory.destroyObject(pair.value);
  50. } catch(Exception e) {
  51. // ignored
  52. }
  53. }
  54. ........

注意: 目前dbcp的pool的实现是使用了公用的apache common pools进行扩展处理,所以和原生的连接池处理,代码看上去有点别扭,感觉自动重连这块异常处理不怎么好,我也就只重点关注了这部分代码而已   .

 

3. dbcp的链接自动重链相关测试

相关场景:

  1. 数据库意外重启后,原先的数据库连接池能自动废弃老的无用的链接,建立新的数据库链接
  2. 网络异常中断后,原先的建立的tcp链接,应该能进行自动切换

测试需求1步骤

  1. 建立一testCase代码
  2. 配置mysql数据库
  3. 循环执行在SQL查询过程
  4. 异常重启mysql数据库

测试需求2步骤

  1. 建立一testCase代码
  2. 配置mysql数据库
  3. 循环执行在SQL查询过程
  4. 通过iptables禁用网络链接

/sbin/iptables -A INPUT -s 10.16.2.69 -j REJECT
/sbin/iptables -A FORWARD -p tcp -s 10.16.2.69 --dport 3306 -m state --state NEW,ESTABLISHED -j DROP

5. iptables -F 清空规则,恢复链接通道。

测试需求问题记录

分别测试了两种配置,有validateObject的配置和没有validateObject的相关配置。

1. 没有validate配置
问题一: 异常重启mysql数据库后,居然也可以自动恢复链接,sql查询正常
跟踪了一下代码,发现这么一个问题:

  1. 在数据库关闭的时候,client中pool通过borrowObject获取一个异常链接返回给client
  2. client在使用具体的异常链接进行sql调用出错了,抛了异常
  3. 在finally,调用connection.close(),本意是应该调用pool通过returnObject返回到的池中,但在跟踪代码时,未见调用GenericObjectPool的returnObject
  4. 继续查,发现在dbcp在中PoolingDataSource(实现DataSource接口)调用PoolableConnection(dbcp pool相关的delegate操作)进行相应关闭时,会检查_conn.isClosed(),针对DataSource如果isClosed返回为true的则不调用returnObject,直接丢弃了链接

解释:

  • 正因为在获取异常链接后,因为做了_conn.isClosed()判断,所以异常链接并没有返回到连接池中,所以到数据库重启恢复后,每次都是调用pool重新构造一个新的connection,所以后面就正常了
  • _conn.isClosed()是否保险,从jdk的api描述中: A connection is closed if the method close has been called on it or if certain fatal errors have occurred. 里面提供两种情况,一种就是被调用了closed方法,另一种就是出现一些异常也说的比较含糊。

问题二:validateObject调用时,dbcp设置的validationQueryTimeout居然没效果

看了mysql statement代码实现,找到了答案。

mysql com.mysql.jdbc.statemen 部分代码

timeout时间处理:

  1. timeoutTask = new CancelTask();
  2. //通过TimerTask启动一定时任务
  3. Connection.getCancelTimer().schedule(timeoutTask,  this.timeoutInMillis);

对应的CancelTask的代码:

  1. class CancelTask extends TimerTask {
  2. long connectionId = 0;
  3. CancelTask() throws SQLException {
  4. connectionId = connection.getIO().getThreadId();
  5. }
  6. public void run() {
  7. Thread cancelThread = new Thread() {
  8. public void run() {
  9. Connection cancelConn = null;
  10. java.sql.Statement cancelStmt = null;
  11. try {
  12. cancelConn = connection.duplicate();
  13. cancelStmt = cancelConn.createStatement();
  14. // 简单暴力,再发起一条KILL SQL,关闭先前的sql thread id
  15. cancelStmt.execute("KILL QUERY " + connectionId);
  16. wasCancelled = true;
  17. } catch (SQLException sqlEx) {
  18. throw new RuntimeException(sqlEx.toString());
  19. } finally {
  20. if (cancelStmt != null) {
  21. try {
  22. cancelStmt.close();
  23. } catch (SQLException sqlEx) {
  24. throw new RuntimeException(sqlEx.toString());
  25. }
  26. }
  27. if (cancelConn != null) {
  28. try {
  29. cancelConn.close();
  30. } catch (SQLException sqlEx) {
  31. throw new RuntimeException(sqlEx.toString());
  32. }
  33. }
  34. }
  35. }
  36. };
  37. cancelThread.start();
  38. }
  39. }

原因总结一句话: queryTimeout的实现是通过底层数据库提供的机制,比如KILL QUERY pid.  如果此时的网络不通,出现阻塞现象,对应的kill命令也发不出去,所以timeout设置的超时没效果。

4.最后

最后还是决定配置testWhileIdle扫描,主要考虑:

  1. pool池中的链接如果未被使用,可以通过testWhileIdle进行链接检查,避免在使用时后总要失败那么一次,可以及时预防
  2. 配合连接池的minEvictableIdleTimeMillis(空闲链接),removeAbandoned(未释放的链接),可以更好的去避免因为一些异常情况引起的问题,防范于未然。比如使用一些分布式数据库的中间件,会有空闲链接关闭的动作,动态伸缩连接池,这时候需要能及时的发现,避免请求失败。
  3. testOnBorrow个人不太建议使用,存在性能问题,试想一下连接一般会在什么情况出问题,网络或者服务端异常终端空闲链接,网络中断你testOnBorrow检查发现不对再取一个链接还是不对,针对空闲链接处理异常关闭,可以从好业务端的重试策略进行考虑,同时配置客户端的空闲链接超时时间,maxIdle,minIdle等。

--------------------------------------------

新加的内容:

5.dbcp密码加密处理

以前使用jboss的jndi数据源的方式,是通过配置oracle-ds.xml,可以设置<security-domain>EncryptDBPassword</security-domain>,引用jboss login-config.xml配置的加密配置。

  1. <application-policy name="EncryptDBPassword">
  2. <authentication>
  3. <login-module code="org.jboss.resource.security.SecureIdentityLoginModule" flag="required">
  4. <module-option name="username">${username}</module-option>
  5. <module-option name="password">${password_encrypt}</module-option>
  6. <module-option name="managedConnectionFactoryName">jboss.jca:service=LocalTxCM,name=${jndiName}</module-option>
  7. </login-module>
  8. </authentication>
  9. </application-policy>

为了能达到同样的效果,切换为spring dbcp配置时,也有类似密码加密的功能,运行期进行密码decode,最后进行数据链接。

实现方式很简单,分析jboss的对应SecureIdentityLoginModule的实现,无非就是走了Blowfish加密算法,自己拷贝实现一份。

  1. private static String encode(String secret) throws NoSuchPaddingException, NoSuchAlgorithmException,
  2. InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
  3. byte[] kbytes = "jaas is the way".getBytes();
  4. SecretKeySpec key = new SecretKeySpec(kbytes, "Blowfish");
  5. Cipher cipher = Cipher.getInstance("Blowfish");
  6. cipher.init(Cipher.ENCRYPT_MODE, key);
  7. byte[] encoding = cipher.doFinal(secret.getBytes());
  8. BigInteger n = new BigInteger(encoding);
  9. return n.toString(16);
  10. }
  11. private static char[] decode(String secret) throws NoSuchPaddingException, NoSuchAlgorithmException,
  12. InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
  13. byte[] kbytes = "jaas is the way".getBytes();
  14. SecretKeySpec key = new SecretKeySpec(kbytes, "Blowfish");
  15. BigInteger n = new BigInteger(secret, 16);
  16. byte[] encoding = n.toByteArray();
  17. Cipher cipher = Cipher.getInstance("Blowfish");
  18. cipher.init(Cipher.DECRYPT_MODE, key);
  19. byte[] decode = cipher.doFinal(encoding);
  20. return new String(decode).toCharArray();
  21. }

最后的配置替换为:

  1. <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
  2. ......
  3. <property name="password"><!-- 注意多了一层转化,将密码串调用decode解密为最初的数据库密码 -->
  4. <bean class="com.xxxxx.EncryptDBPasswordFactory">
  5. <property name="password" value="${xxxx.password.encrypted}" />
  6. </bean>
  7. </property>
  8. ........
  9. </bean>

--------------------------------------------

新加的内容:

6.数据库重连机制

常见的问题:

1. 数据库意外重启后,原先的数据库连接池能自动废弃老的无用的链接,建立新的数据库链接

2. 网络异常中断后,原先的建立的tcp链接,应该能进行自动切换。比如网站演习中的交换机重启会导致网络瞬断

3. 分布式数据库中间件,比如amoeba会定时的将空闲链接异常关闭,客户端会出现半开的空闲链接。

大致的解决思路:

1. sql心跳检查

主动式 ,即我前面提到的sql validate相关配置

2. 请求探雷

牺牲小我,完成大我的精神。 拿链接尝试一下,发现处理失败丢弃链接,探雷的请求总会失败几个,就是前面遇到的问题一,dbcp已经支持该功能,不需要额外置。

3. 设置合理的超时时间,

解决半开链接. 一般数据库mysql,oracle都有一定的链接空闲断开的机制,而且当你使用一些分布式中间件(软件一类的),空闲链接控制会更加严格,这时候设置合理的超时时间可以有效避免半开链接。

一般超时时间,dbcp主要是minEvictableIdleTimeMillis(空闲链接) , removeAbandonedTimeout(链接泄漏)。可以见前面的参数解释。

dbcp相关配置的更多相关文章

  1. dbcp基本配置和重连配置 -- mysql 8小时自动断开连接的问题

    1. 引入dbcp (选择1.4) Java代码   com.alibaba.external jakarta.commons.dbcp 1.4 2. dbcp的基本配置 相关配置说明: initia ...

  2. 【DBCP】DBCP基本配置和重连配置+spring中配置

    最近在看一些dbcp的相关内容,顺便做一下记录,免得自己给忘记了.   1. 引入dbcp (选择1.4) <dependency> <groupId>com.alibaba. ...

  3. dbcp 详细配置

    1.配置参数 username : 连接用户名 password:  连接密码 url :  连接 url( 如果连接 mysql ,格式为 jdbc:mysql://ip:port/dbname) ...

  4. dbcp基本配置和重连配置

    转载自:http://agapple.iteye.com/blog/772507 最近在看一些dbcp的相关内容,顺便做一下记录,免得自己给忘记了. 1. 引入dbcp (选择1.4) Java代码  ...

  5. SpringMVC之application-context.xml,了解数据库相关配置

    上一篇SpringMVC之web.xml让我们了解到配置一个web项目的时候,怎样做基础的DispatcherServlet相关配置.作为SpringMVC上手的第一步.而application-co ...

  6. zookeeper集群的搭建以及hadoop ha的相关配置

    1.环境 centos7 hadoop2.6.5 zookeeper3.4.9 jdk1.8 master作为active主机,data1作为standby备用机,三台机器均作为数据节点,yarn资源 ...

  7. Linux网络相关配置

    一.修改网卡相关配置 Linux网络参数是在/etc/sysconfig/network-scripts/ifcfg-eth0中设置,其中ifcfg-eth0表示是第一个网卡,如果还有另外一块网卡,则 ...

  8. ios开发之Info.plist文件相关配置

    前言:在iOS开发中有些情况下需要对Info.plist文件进行配置,以下介绍几种相关配置.以后遇到需要配置的再更新... 开发环境:swift3.0.1,Xcode8.1 一,项目中需要使用第三方字 ...

  9. SharePoint 2013 托管导航及相关配置 <二>

    本文的思路是使用JQuery重写SharePoint自带托管导航的样式,其实思路和脚本都非常简单,引用一下JQuery脚本,然后重写导航的样式,把脚本放到母版页中,即可.当然,使用JQuery可以做很 ...

随机推荐

  1. Vue push() pop() shift() unshift() splice() sort() reverse() ...

    Vue 变异方法 push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度. pop() 方法用于删除并返回数组的最后一个元素. shift() 方法用于把数组的第一个元素从其中删除,并返回 ...

  2. 环境变量、system(day10)

    一.环境变量 bash下的环境变量. 每个进程都默认从父进程继承环境变量 bash本身就是一个程序,这个程序运行的时候,bash进程 可以定义只能之自己这个进程中使用的变量,这种变量称为自定义变量. ...

  3. Hexo系列(四) NexT主题配置

    Hexo 框架允许我们更换合适的主题,以便于构建不同风格的网站,这里介绍目前最常使用的一款主题之一 -- NexT 一.NexT 安装 在正式开始讲解 NexT 安装之前,我们必须明确以下几个概念: ...

  4. 用户输入input函数和代码注释

    一.读取用户输入 py3中input()读取用户输入,输出全部是默认str字符串数据类型,一般将其赋值变量,用户输入才继续往下走程序.(py2的不同已单独列出随笔) 二.注释 注释的作用:代码量大的时 ...

  5. Python之Django的Model2

    一.创建数据库 创建数据库 进入数据库: mysql -uroot -p 创建数据库: CREATE DATABASE test1 CHARSET=utf8; 连接数据库 虚拟环境中安装数据库模块:p ...

  6. 使用SQLAlchemy对博客文章进行分页

    https://blog.csdn.net/hyman_c/article/details/54382161

  7. CSS行高line-height的学习

    一.定义和用法 line-height 属性设置行间的距离(行高). 可能的值 normal默认.设置合理的行间距. number设置数字,此数字会与当前的字体尺寸相乘来设置行间距. length设置 ...

  8. MySQL用Load Data local infile 导入部分数据后中文乱码

    今天在两台MySQL服务器之间导数据,因为另一个MySQL服务器是测试用的,差一个月的数据,从现有MySQL服务器select到一个文件,具体语句是: select * from news where ...

  9. 先验概率 vs 后验概率

    其实还不是很懂.看了这篇文章: http://blog.csdn.net/passball/article/details/5859878   事情还没有发生,要求这件事情发生的可能性的大小,是先验概 ...

  10. iOS开发——定制圆形头像与照相机图库的使用

    如今的App都很流行圆形的头像,比方QQ右上角的头像,今日头条的头像等等.这已经成为App设计的趋势了.今天我们就来简单实现一下这个功能,我还会把从手机拍照中或者图库中取出作为头像的照片存储到应用程序 ...