原理

  AOP(Aspect Oriented Programming),也就是面向方面编程的技术。AOP基于IoC基础,是对OOP的有益补充。AOP将应用系统分为两部分,核心业务逻辑(Core business concerns)及横向的通用逻辑,也就是所谓的方面Crosscutting enterprise concerns,例如,所有大中型应用都要涉及到的持久化管理(Persistent)、事务管理(Transaction Management)、安全管理(Security)、日志管理(Logging)和调试管理(Debugging)等。
  AOP正在成为软件开发的下一个光环。使用AOP,你可以将处理aspect的代码注入主程序,通常主程序的主要目的并不在于处理这些aspect。AOP可以防止代码混乱。Spring framework是很有前途的AOP技术。作为一种非侵略性的、轻型的AOP framework,你无需使用预编译器或其他的元标签,便可以在Java程序中使用它。这意味着开发团队里只需一人要对付AOP framework,其他人还是像往常一样编程。

拦截器(也称拦截机)

  拦截机 (Interceptor), 是 AOP (Aspect-Oriented Programming) 的另一种叫法。AOP本身是一门语言,只不过我们使用的是基于JAVA的集成到Spring 中的 SpringAOP。同样,我们将通过我们的例子来理解陌生的概念。接口类

public interface UserService {// 被拦截的接口
public void printUser(String user);
}

  实现类

public class UserServiceImp implements UserService {// 实现UserService接口
public void printUser(String user) ...{
System.out.println("printUser user:" + user);// 显示user
}
}

   AOP拦截器

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class UserInterceptor implements MethodInterceptor{// AOP方法拦截器
public Object invoke(MethodInvocation arg0) throws Throwable {
try {
if (arg0.getMethod().getName().equals("printUser")){
// 拦截方法是否是UserService接口的printUser方法
Object[] args = arg0.getArguments();// 被拦截的参数
System.out.println("user:" + args[0]);
arg0.getArguments()[0] = "hello!";// 修改被拦截的参数
}
System.out.println(arg0.getMethod().getName() + "---!");
return arg0.proceed();// 运行UserService接口的printUser方法
}catch (Exception e) ...{
throw e;
}
}
}

  测试类

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.context.support.WebApplicationContextUtils; public class TestInterceptor{
public static void main(String[] args){
ApplicationContext ctx = new FileSystemXmlApplicationContext(
"classpath:applicationContext.xml");
// ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService us = (UserService) ctx.getBean("userService");
us.printUser("shawn");
}
}

  配置文件

<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="userServiceImp"
class="com.test.TestSpring3.UserServiceImp" />
<bean id="userInterceptor" class="com.test.TestSpring3.UserInterceptor" />
<bean id="userService"
class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 代理接口 -->
<property name="proxyInterfaces">
<value>com.test.TestSpring3.UserService</value>
</property>
<!-- 目标实现类 -->
<property name="target">
<ref local="userServiceImp" />
</property>
<!-- 拦截器 -->
<property name="interceptorNames">
<list>
<value>userInterceptor</value>
</list>
</property>
</bean>
</beans>

  输出:

  user:shawn
printUser---!
printUser user:hello!

  结论:调用方法的时候 传入的值被拦截修改了.

拦截器中的事务管理(事务拦截机)

  如果不采用拦截机的机制时,在使用JDBC进行数据库访问时,存在两种情况:

  • 自动提交.这是JDBC驱动默认的模式,每次数据库操作(CRUD)成功完成后,都作为一个单独的事务自动提交,如果未成功完成,即抛出了 SQLException 的话,仅最近的一个操作将回滚。
  • 非自动提交,这是想更好的控制事务时需要程序地方式进行控制:
    • 在进行该事务单元的任何操作之前 setAutoCommit(false)
    • 在成功完成事务单元后 commit()
    • 在异常发生后 rollback()

  自动提交模式是不被推荐的,因为每个操作都将产生一个事务点,这对于大的应用来说性能将受到影响;再有,对于常见的业务逻辑,这种模式显得无能为力。比如:转帐,从A帐户取出100元,将其存入B帐户;如果在这两个操作之间发生了错误,那么用户A将损失了100元,而本来应该给帐户B的,却因为失败给了银行。所以,建议在所有的应用中,如果使用 JDBC 都将不得不采用非自动提交模式(你们要能发现了在我们的 JDBC 那个例子中,我们采用的就是自动提交模式,我们是为了把精力放在JDBC上,而不是事务处理上),即我们不得不在每个方法中:

  这样代码在AOP的倡导者看来是“肮脏”的代码。他们认为,所有的与事务有关的方法都应当可以集中配置(见声明性事务控制),并自动拦截,程序应当关心他们的主要任务,即商业逻辑,而不应和事务处理的代码搅和在一起。
我先看看 Spring 是怎么做到拦截的:

Spring 内置支持的事务处理拦截机

这里因为要用到JpetStore项目中的代码,我们将 applicationContext.xml 全部内容列出:

<?xml version="1.0" encoding="UTF-8"?>

<!--
- Application context definition for JPetStore's business layer.
- Contains bean references to the transaction manager and to the DAOs in
- dataAccessContext-local/jta.xml (see web.xml's "contextConfigLocation"). Jpetstore 的应用上下文定义,包含事务管理和引用了在 dataAccessContext-local/jta.xml(具体使用了哪个要看 web.xml 中的 'contextConfigLocation' 的配置)中注册的DAO
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd"> <!-- ========================= GENERAL DEFINITIONS ========================= --> <!-- Configurer that replaces ${...} placeholders with values from properties files
占位符的值将从列出的属性文件中抽取出来
-->
<!-- (in this case, mail and JDBC related properties) -->
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>WEB-INF/mail.properties</value>
<value>WEB-INF/jdbc.properties</value>
</list>
</property>
</bean> <!-- MailSender used by EmailAdvice
指定用于发送邮件的 javamail 实现者,这里使用了 spring 自带的实现。此 bean 将被 emailAdvice 使用
-->
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="${mail.host}"/>
</bean> <!-- ========================= BUSINESS OBJECT DEFINITIONS ======================== --> <!-- 不需要,因为被 SpringMVC 的实现使用 Generic validator for Account objects, to be used for example by the Spring web tier -->
<bean id="accountValidator" class="org.springframework.samples.jpetstore.domain.logic.AccountValidator"/> <!-- 不需要,因为被 SpringMVC 的实现使用 Generic validator for Order objects, to be used for example by the Spring web tier -->
<bean id="orderValidator" class="org.springframework.samples.jpetstore.domain.logic.OrderValidator"/> <!--
主要的商业逻辑对象,即我们所说的门面对象
注入了所有的DAO,这些DAO是引用了 dataAccessContext-xxx.xml 中定义的DAO
门面对象中的所有方法的事务控制将通过下面的 aop:config 来加以控制 - JPetStore primary business object (default implementation).
- Transaction advice gets applied through the AOP configuration below.
-->
<bean id="petStore" class="org.springframework.samples.jpetstore.domain.logic.PetStoreImpl">
<property name="accountDao" ref="accountDao"/>
<property name="categoryDao" ref="categoryDao"/>
<property name="productDao" ref="productDao"/>
<property name="itemDao" ref="itemDao"/>
<property name="orderDao" ref="orderDao"/>
</bean> <!-- ========================= ASPECT CONFIGURATION ======================== -->
<!-- AOP配置,用来控制哪些方法将需要进行事务处理,采用了AspectJ 的语法 -->
<aop:config>
<!--
This definition creates auto-proxy infrastructure based on the given pointcut,
expressed in AspectJ pointcut language. Here: applying the advice named
"txAdvice" to all methods on classes named PetStoreImpl.
-->
<!-- 指出在 PetStoreFacade 的所有方法都将采用 txAdvice(在紧接着的元素中定义了)事务方针,注意,我们这里虽然指定的是接口 PetStoreFacace, 但其暗示着其所有的实现类也将
        同样具有这种性质,因为本身就是实现类的方法在执行的,接口是没有方法体的。 -->
<aop:advisor pointcut="execution(* *..PetStoreFacade.*(..))" advice-ref="txAdvice"/> <!--
This definition creates auto-proxy infrastructure based on the given pointcut,
expressed in AspectJ pointcut language. Here: applying the advice named
"emailAdvice" to insertOrder(Order) method of PetStoreImpl
-->
<!-- 当执行 PetStoreFacade.insertOrder方法,该方法最后一个参数为Order类型时(其实我们的例子中只有一个 insertOrder 方法,但这告诉了我们,当我们的接口或类中有重载了的方法,
        并且各个重载的方法可能使用不同的拦截机机制时,我们可以通过方法的参数加以指定),将执行emailAdvice(在最后定义的那个元素)-->
<aop:advisor pointcut="execution(* *..PetStoreFacade.insertOrder(*..Order))" advice-ref="emailAdvice"/> </aop:config> <!--
     
事务方针声明,用于控制采用什么样的事务策略
Transaction advice definition, based on method name patterns.
Defaults to PROPAGATION_REQUIRED for all methods whose name starts with
"insert" or "update", and to PROPAGATION_REQUIRED with read-only hint
for all other methods.
-->
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="insert*"/>
<tx:method name="update*"/>
<tx:method name="*" read-only="true"/>
</tx:attributes>
</tx:advice> <!-- 拦截机,用于在适当的时机(通过AOP配置,如上面)在方法执行成功后发送邮件
AOP advice used to send confirmation email after order has been submitted -->
<!-- -->
<bean id="emailAdvice" class="org.springframework.samples.jpetstore.domain.logic.SendOrderConfirmationEmailAdvice">
<property name="mailSender" ref="mailSender"/>
</bean> <!-- ========================= 忽略 REMOTE EXPORTER DEFINITIONS ======================== --> </beans>

  这个配置比想象的要简单的多:

<aop:config>
<!-- This definition creates auto-proxy infrastructure based on the given pointcut, expressed in AspectJ pointcut language.
Here: applying the advice named "txAdvice" to all methods on classes named PetStoreImpl. 指出在 PetStoreFacade
的所有方法都将采用 txAdvice(在紧接着的元素中定义了)事务方针,注意,我们这里虽然指定的是接口 PetStoreFacace,
但其暗示着其所有的实现类也将同样具有这种性质,因为本身就是实现类的方法在执行的,接口是没有方法体的。 -->
<aop:advisor pointcut="execution(* *..PetStoreFacade.*(..))" advice-ref="txAdvice"/>
<!-- 其它拦截机-->
</aop:config>

  1. 所有的拦截机配置都放在 <aop:config> 配置元素中.
  2. 下面还是需要理解一下几个有关AOP的专用名词,不过,是挺抽象的,最好能会意出其的用意

  • pointcut 切入点,比如:updateAccount 方法需要进行事务管理,则这个切入点就是“执行方法体”(execution)。Spring 所有支持的切入点类型在都在 Spring reference: 6.2.3.1. Supported Pointcut Designators 中列出了。
  • advice要对这个切入点进行什么操作,比如事务控制
  • advisor 。Spring 特有的概念,将上两个概念合到一个概念中来,即一个 advisor 包含了一个切入点及对这个切入点所实施的操作。

  因为方法执行切入点 execution 为最常见的切入点类型,我们着重介绍一下,execution 的完全形式为:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

这是一个正则表达式,其中由 '?' 结尾的部分是可选的。翻译过来就是:

执行(方法访问修饰符? 方法返回类型 声明类型? 方法名(方法参数类型) 抛出异常?)

所有的这些都是用来定义执行切入点,即那些方法应该被侯选为切入点:

  • 方法访问修饰符   即 public, private 等等
  • 方法返回类型       即方法返回的类型,如 void, String 等等
  • 声明类型                1.5的语法,现在可以先忽略它
  • 方法名                    方法的名字
  • 方法参数类型       方法的参数类型
  • 抛出异常                方法声明的抛出的异常

例如,所有dao代码被定义在包 com.xyz.dao 及子包 com.xyz.dao.hibernate, 或者其它,如果还有的话,子包中, 里面定义的是提供DAO功能的接口或类,那么表达式:

execution(* com.xyz.dao..*.*(..))

表示切入点为:执行定义在包 com.xyz.dao 及其子包(因为 .. 所致) 中的任何方法

详细情况可以参见 Spring refernce: 6.2.3.4. Examples
因此这个表达式为执行定义在类 PetStoreFacade 及其实现类中的所有方法,采取的动作定义在 txAdvice 中. 关于该 advice 的定义,(见声明性事务控制)一节

<aop:advisor pointcut="execution(* *..PetStoreFacade.*(..))" advice-ref="txAdvice"/> 

Spring 自定拦截机

  来为了进行事务控制,我们只需简单地配置几下,所有的工作都由 Spring 来做。这样固然很好,但有时我们需要有我们特有的控制逻辑。因为Spring 不可能包含所有人需要的所有拦截机。所以它提供了通过程序的方式加以定制的方式。我们的项目中就有这么一个拦截机,在用户确认付款后,将定单信息通过 email 的方式发送给注册用户的邮箱中。


<aop:config>
<!-- 当执行 PetStoreFacade.insertOrder方法,该方法最后一个参数为Order类型时(其实我们的例子中只有一个 insertOrder 方法,但这告诉了我们,
当我们的接口或类中有重载了的方法,并且各个重载的方法可能使用不同的拦截机机制时,我们可以通过方法的参数加以指定),将执行emailAdvice(在最后定义的那个元素)-->
<aop:advisor pointcut="execution(* *..PetStoreFacade.insertOrder(*..Order))" advice-ref="emailAdvice"/>
</aop:config>

  红色的注释已经说的很清楚这个 Advisor 了,它的切入点(pointcut) 为 PetStoreFacade 的 void insertOrder(Order order) 方法,采取的动作为引用的 emailAdvice, 下面我们就来看看 emailAdvice:

<bean id="emailAdvice" class="org.springframework.samples.jpetstore.domain.logic.SendOrderConfirmationEmailAdvice">
<property name="mailSender" ref="mailSender"/>
</bean>

  它给了这个 advice 的实现类为 logic 包中 SendOrderConfirmationEmailAdvice, 该Bean 引用了我们前面定义的邮件发送器(一个 Spring 内置的邮件发送器).下面看看这个实现类:


public class SendOrderConfirmationEmailAdvice implements AfterReturningAdvice, InitializingBean {
// user jes on localhost
private static final String DEFAULT_MAIL_FROM = "test@pprun.org";
private static final String DEFAULT_SUBJECT = "Thank you for your order!";
private final Log logger = LogFactory.getLog(getClass());
private MailSender mailSender;
private String mailFrom = DEFAULT_MAIL_FROM;
private String subject = DEFAULT_SUBJECT;
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
} public void setMailFrom(String mailFrom) {
this.mailFrom = mailFrom;
} public void setSubject(String subject) {
this.subject = subject;
} public void throws Exception {
if (this.mailSender == null) {
throw new IllegalStateException("mailSender is required");
}
} /**
*
* @param returnValue 被拦截的方法的返回值
* @param m 被拦截的方法的所有信息(Method类封装了这些信息)
* @param args 被拦截的方法的所有参数组成的数组
* @param target 目标对象,对于方法执行来说,即是方法所在的类的实例(与 this 同,批当前对象)
* @throws java.lang.Throwable
*/
public void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable {
// 我们被拦截的方法为 void insertOrder(Order order),方法只有一个参数,所以可知数据的第1个元素即是被传进的 order 对象
// 得到了order 对象,就可以将 order 对应的帐户名及帐单号发送到邮件中,以便确认无误。
Order order = (Order) args[0];
Account account = ((PetStoreFacade) target).getAccount(order.getUser().getUsername()); // don't do anything if email address is not set
if (account.getEmail() == null || account.getEmail().length() == 0) {
return;
} StringBuffer text = new StringBuffer();
text.append("Dear ").append(account.getFirstname()).
append(' ').append(account.getLastname());
text.append(", thank your for your order from JPetStore. " +
"Please note that your order number is ");
text.append(order.getId()); SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(account.getEmail());
mailMessage.setFrom(this.mailFrom);
mailMessage.setSubject(this.subject);
mailMessage.setText(text.toString());
try {
this.mailSender.send(mailMessage);
} catch (MailException ex) {
// just log it and go on
logger.warn("An exception occured when trying to send email", ex);
}
}
}

  1. 红色的内容即为反向注入的 mailSender 属性
  2. 蓝色的内容为 Spring Bean 的一个通用的接口 InitializingBean ,实现类需要实现该接口定义的方法 afterPropertiesSet() ,该方法中一般是在Bean 被初始化后并设置了所有的 setter 注入后调用的。所以这里是保证邮件发送器配置正确。因为如果没有配置正确,下面的工作是无法进行的,所以与其等那时抛出异常,还不如早早地在部署时就告知(通过抛出 IllegalStateException 来提示)
  3. 绿色的内容为这个 Advise 的核心,即在切入点被切入后将采用的动作。因为 Advise 也同样有多种类型,比如我们这里的“方法正常返回”,“方法执行前”,“方法执行后”,“环绕在方法执行前后”,“方法抛出异常时”等等(详情参见 Spring Reference: 6.2.4. Declaring advice)。但是我们的逻辑为在用户确认定单并且执行成功(所谓的成功是指将这一定单插入到了表 Order 中了)后,将发送一确认信。所以”方法正常返回“完全符合我们的要求。
  接口AfterReturningAdvice 即是 Spring中表示”方法正常返回“ 这一语义的 Advice, 所以我们实现这个接口及其必须的方法 afterReturning.
  方法代码的工作其实并不重要,只要我们理解这些“魔法”一样的技术后,实现代码是很简单的。值得提及的是这个方法的参数,这些参数是封装了切入点的所有信息,请见上面的注释。在我们的实现中只使用了被拦截方法的参数,在复杂的 Advice 实现中可能会用到切入点所有信息。

SpringAOP原理的更多相关文章

  1. SpringAOP原理分析

    目录 Spring核心知识 SpringAOP原理 AOP编程技术 什么是AOP编程 AOP底层实现原理 AOP编程使用 Spring核心知识 Spring是一个开源框架,Spring是于2003年兴 ...

  2. SpringAOP 原理解析

    什么是AOP? 1: 传统的OOP编程他的代码逻辑是一种自上向下, 而在这些自上而下的过程中会产生一些横切性的问题,比如说:日志信息,权限校验认证,事务等, 2: 这些横切性问题,往往与我们的主业务逻 ...

  3. springAOP原理以及概念

    需求:1.拦截所有业务方法2.判断用户是否有权限,有权限就让他执行业务方法,没有权限就不允许执行.(是否有权限是根据user是否为null作为判断依据) 思考: 我们该如何实现? 思路1: 我们在每个 ...

  4. 1.springAOP原理分析

    环境:jdk1.8 + spring boot 2.0.9.RELEASE Spring AOP的实现本质上就是代理Proxy + 一系列的拦截器 使用@Aspect,引入依赖 <depende ...

  5. [原创]java WEB学习笔记104:Spring学习---AOP 前奏,通过一个问题引入动态代理

    本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱 ...

  6. Spring aop 小例子demo

    由于最近的服务项目提供接口有一个需求,所有操作都必须检查操作的服务可用,所以感觉Aop特别适合实施.完成学习的小例子. 关于spring-Aop原理:http://m.oschina.net/blog ...

  7. Spring注解驱动开发之AOP

    前言:现今SpringBoot.SpringCloud技术非常火热,作为Spring之上的框架,他们大量使用到了Spring的一些底层注解.原理,比如@Conditional.@Import.@Ena ...

  8. 不依赖Spring使用AspectJ达到AOP面向切面编程

    网上大多数介绍AspectJ的文章都是和Spring容器混用的,但有时我们想自己写框架就需要抛开Spring造轮子,类似使用原生AspectJ达到面向切面编程.步骤很简单,只需要两步. 1.导入依赖 ...

  9. AOP与IOC区别

    Spring核心知识 Spring是一个开源框架,Spring是于2003年兴起的一个轻量级的Java开发框架,由Rod Johnson在其著作Expert One-On-One J2EE Devel ...

随机推荐

  1. 如何使用ArcGIS发布LiDAR 点云

    LiDAR--Light Detection And Ranging,即激光探测与测量技术. 下面将介绍如何使用ARCGIS来发布LiDAR的成果点云数据. LiDAR的点云数据一般格式为LAS.在A ...

  2. Servlet实现的三种方式

    实现Servlet的三种方式:一个实现,两个继承 /*========================================== * servlet的执行过程: * 1.创建servlet对 ...

  3. 【C语言】判断三角形类型

    根据输入的三角形的三边判断三角形的类型,并输出其面积和类型. #include<stdio.h> #include<stdlib.h> #include<math.h&g ...

  4. linux系统使用python监测网络接口获取网络的输入输出

    #!/usr/bin/env Pythonimport timeimport sys if len(sys.argv) > 1: INTERFACE = sys.argv[1]else: INT ...

  5. 字符串的模式匹配(Java实现)

    字符串的模式匹配 字串的定位操作通常称做模式匹配,是各种串处理系统中最重要的操作之一.本文主要介绍两种常用的实现算法: 1.暴力匹配 2.KMP算法 1.暴力匹配 时间复杂度为O(n*m):n为主串长 ...

  6. 老李秘技:loadrunner11.5支持net4.0么?

    老李秘技:loadrunner11.5支持net4.0么?   LoadRunner12.0以前的版本不支持.NET 4.0,已经证实R&D团队将在下一版本的LoadRunner即LoadRu ...

  7. ArcGIS API for JavaScript 4.2学习笔记[20] 使用参数查询要素(油井和地震关系)

    这个例子相当复杂.我先简单说说这个例子是干啥的. 在UI上,提供了一个下拉框.两个滑动杆,以确定三个参数,使用这三个参数进行空间查询.这个例子就颇带空间查询的意思了. 这个例子是干嘛的呢?第一个参数是 ...

  8. Hibernate基础学习(五)—对象-关系映射(下)

    一.单向n-1 单向n-1关联只需从n的一端可以访问1的一端. 域模型: 从Order到Customer的多对一单向关联.Order类中定义一个Customer属性,而在Customer类不用存放Or ...

  9. LwIP之socket应用--WebServer和Modbus TCP

    1. 引言 LwIP是嵌入式领域一个流行的以太网协议栈, LwIP开放源码,用C写成非常方便移植,并且支持socket接口,使用者可以集中精力处理应用功能. 本文就是LwIP socket使用的一个小 ...

  10. 如何在Windows系统下安装Linux虚拟机

    先安装虚拟机这个软件,然后在虚拟机里装linux. 1,准备,下载VM虚拟机,链接: http://pan.baidu.com/s/1z79oU 密码: vbap.和linux镜像文件,可以下载ubu ...