文章目录
- 1、代理模式基本介绍
- 2、Jdk中的动态代理
- 2.1、场景推导
- 2.2、Jdk动态代理
- 3、静态代理
- 4、代理模式的关键点
- 5、代理模式和适配器模式的比较
- 6、代理模式UML图
1、代理模式基本介绍
代理模式的定义:
- 为其他对象提供一种代理以
控制对这个对象的访问
。 - 在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
简单来说
-
代理模式就是代理对象具备真实对象的功能,并代替真实对象完成相应操作,
并能够在操作执行的前后,对操作进行增强处理
。 -
为真实对象提供代理,然后供其他对象通过代理访问真实对象
2、Jdk中的动态代理
2.1、场景推导
实现一个简单的加减乘除运算功能
interface ICalc{
int add(int a,int b);
int sub(int a,int b);
int mul(int a,int b);
int div(int a,int b);
}
class CalcImpl implements ICalc{
@Override
public int add(int a, int b) {
return a+b;
}
@Override
public int sub(int a, int b) {
return a-b;
}
@Override
public int mul(int a, int b) {
return a*b;
}
@Override
public int div(int a, int b) {
return a/b;
}
}
class AppTest{
public static void main(String[] args) {
CalcImpl c = new CalcImpl();
System.out.println(c.add(4,2));
System.out.println(c.sub(4,2));
System.out.println(c.mul(4,2));
System.out.println(c.div(4,2));
}
}
现在变化
来了,客户要求为每个方法添加日志,记录方法开始和结束的时机
package com.hh.demo.designpattern;
interface ICalc{
int add(int a,int b);
int sub(int a,int b);
int mul(int a,int b);
int div(int a,int b);
}
class CalcImpl implements ICalc{
@Override
public int add(int a, int b) {
System.out.println("add方法开始!" +"a="+a+"b="+b);
int r = a+b;
System.out.println("add方法结束!" +"r="+r);
return r;
}
@Override
public int sub(int a, int b) {
System.out.println("sub方法开始!" +"a="+a+"b="+b);
int r = a-b;
System.out.println("sub方法结束!" +"r="+r);
return r;
}
@Override
public int mul(int a, int b) {
System.out.println("mul方法开始!" +"a="+a+"b="+b);
int r = a*b;
System.out.println("mul方法结束!" +"r="+r);
return r;
}
@Override
public int div(int a, int b) {
System.out.println("div方法开始!" +"a="+a+"b="+b);
int r = a/b;
System.out.println("div方法结束!" +"r="+r);
return r;
}
}
class AppTest{
public static void main(String[] args) {
CalcImpl c = new CalcImpl();
System.out.println(c.add(4,2));
System.out.println(c.sub(4,2));
System.out.println(c.mul(4,2));
System.out.println(c.div(4,2));
}
}
梭哈搞定,打完收工!
我们发现,这样完成业务根本不是一个好办法
- 代码在重复,核心业务(加减乘除)和非核心业务(打印日志)在不断的重复。
- 如果Icalc和CalcImpl不是我们自己创建的,是被发现的,那我们手里是没有源代码的,不能直接修改源代码(开闭原则)
- 需求如果再次变化,需要加入开方,求余的过程 ;又或者客户要求 上午需要日志,下午不需要日志!!!
我们尝试使用动态代理
来完成上述功能
2.2、Jdk动态代理
Jdk动态代理:在程序的执行过程中,使用jdk的反射机制,创建代理对象,并动态的指定代理的目标类
先来看看业务逻辑是怎么实现的
package com.hh.demo.designpattern;
interface ICalc {
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
}
class CalcImpl implements ICalc {
@Override
public int add(int a, int b) {
int r = a + b;
return r;
}
@Override
public int sub(int a, int b) {
int r = a - b;
return r;
}
@Override
public int mul(int a, int b) {
int r = a * b;
return r;
}
@Override
public int div(int a, int b) {
int r = a / b;
return r;
}
}
//调用处理器
class MyHandler implements InvocationHandler {
//关联
private ICalc calculator;
public MyHandler(ICalc calculator) {
this.calculator = calculator;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName()+"开始,参数:"+ Arrays.toString(args));
//利用反射机制,调用方法
//把method所代表的方法,当作calculator对象的调用,参数是args
//Object是为了通用
Object res = method.invoke(calculator, args);
System.out.println(method.getName()+"结束,结果是:"+ res);
return res;//这个返回值会返回到代理对象的方法调用处
}
}
public class AppTest {
public static void main(String[] args) {
ICalc calculator = new CalcImpl();
//当前类的字节码获得当前类的类加载器
ClassLoader classLoader = AppTest.class.getClassLoader();
//创建代理对象,需要传入三个参数
ICalc proxy = (ICalc) Proxy.newProxyInstance(classLoader, new Class[]{ICalc.class}, new MyHandler(calculator));
//总之,对代理对象的方法的调用,都统统会进入调用处理器中
proxy.add(3, 2);
proxy.sub(3, 2);
proxy.mul(3, 2);
proxy.div(3, 2);
}
/**
* add开始,参数:[3, 2]
* add结束,结果是:5
* sub开始,参数:[3, 2]
* sub结束,结果是:1
* mul开始,参数:[3, 2]
* mul结束,结果是:6
* div开始,参数:[3, 2]
* div结束,结果是:1
*
* Process finished with exit code 0
*/
}
我们先来看看动态代理api
Proxy.newProxyInstance();
里面传三个参数,分别是:
-
第一个参数:
-
实例化一个对象,必然会调用类的构造器。在调用构造器之前,Jvm会加载该类的字节码,而Jvm就是使用类加载器来加载类的字节码,这一步是Jvm自动完成的。
-
简单来说:只要实例化对象,一定要加载类的字节码,加载字节码就一定要类的加载器。
-
使用
动态代理
的api实例化对象是一种不常用的创建对象的方式,但这也是一种实例化,需要我们手动把类的加载器传入
-
使用构造器实例化对象时Jvm会自动找到类加载器。
-
-
第二个参数:
-
第一个参数传入的类加载器,加载的是哪个类的字节码?加载的字节码就是在运行期动态生成的字节码,这个动态生成的字节码是不需要源代码的。
-
字节码确实可以自动生成,那么动态代理api成成的字节码的内容,是根据什么生成的呢?恰恰是根据第二个参数生成的。动态生成代理,会生成一个实现了目标接口的类的字节码,在上面的栗子中就是生成了一个ICalc接口的类的字节码!
-
-
第三个参数:调用处理器 InvocationHandler
- 我们已经知道,动态代理会加载自己动态生成的字节码,且这个字节码是根据某个接口生成的,在上面的例子中就是根据ICalc接口生成的实现了ICalc接口的类的字节码
- 实现一个接口,就要实现其中的抽象方法,那麽动态代理生成的字节码,实现了ICalc接口,必然就要实现其中的add、sub等方法
- 这些方法被实现的方法体是什么内容呢?这恰恰是由第三个参数决定的,MyHandler类的 invoke方法,就是方法体的内容!!
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
第一个参数:动态代理的对象
第二个参数:调用的接口方法
第三个参数:调用的接口方法的参数
}
但是目前这个写法还是有缺点的,太复杂了,对于新手不是很友好,我们来封装一下
package com.hh.demo.designpattern;
interface ICalc {
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
}
class CalcImpl implements ICalc {
@Override
public int add(int a, int b) {
int r = a + b;
return r;
}
@Override
public int sub(int a, int b) {
int r = a - b;
return r;
}
@Override
public int mul(int a, int b) {
int r = a * b;
return r;
}
@Override
public int div(int a, int b) {
int r = a / b;
return r;
}
}
class MyHandler implements InvocationHandler {
//关联
private Object target;
public MyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName()+"开始,参数:"+ Arrays.toString(args));
//利用反射机制,调用方法
//把method所代表的方法,当作calculator对象的调用,参数是args
//Object是为了通用
Object res = method.invoke(target, args);
System.out.println(method.getName()+"结束,结果是:"+ res);
return res;//这个返回值会返回到代理对象的方法调用处
}
}
//----------------------------------------------------------------------------------------------------
class MyProxy{
//封装:对外隐藏复杂的实现细节,暴露出简单的使用方法
public Object getProxy(Object target){
//当前类的字节码获得当前类的类加载器
ClassLoader classLoader = MyProxy.class.getClassLoader();
//获取target所属的类,所实现的接口
Class<?>[] interfaces = target.getClass().getInterfaces();
//创建代理对象,需要传入三个参数
Object proxy = Proxy.newProxyInstance(classLoader,interfaces, new MyHandler(target));
return proxy;
}
}
public class AppTest {
public static void main(String[] args) {
ICalc calculator = new CalcImpl();
ICalc proxy = (ICalc) new MyProxy().getProxy(calculator);
proxy.add(3, 2);
proxy.sub(3, 2);
proxy.mul(3, 2);
proxy.div(3, 2);
}
/**
* add开始,参数:[3, 2]
* add结束,结果是:5
* sub开始,参数:[3, 2]
* sub结束,结果是:1
* mul开始,参数:[3, 2]
* mul结束,结果是:6
* div开始,参数:[3, 2]
* div结束,结果是:1
*
* Process finished with exit code 0
*/
}
MyProxy类对外隐藏复杂的实现细节,暴露出简单的使用方法。似乎有点代理的意思了
目前看起来似乎挺好的,但是仍然有问题:
目前我们创建的代理对象,只能在真实对象的真实方法调用前后加上日志,无法扩展其他功能,比如,用户不想加日志功能,而是想加缓存功能,或者权限控制…
再次封装代码,我们定义一个接口,用来描述代理类对应方法执行前后需要拓展执行的方法。
称这个接口为Interceptor
拦截器,也可以理解为切面
package com.hh.demo.designpattern;
interface ICalc {
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
}
class CalcImpl implements ICalc {
@Override
public int add(int a, int b) {
int r = a + b;
return r;
}
@Override
public int sub(int a, int b) {
int r = a - b;
return r;
}
@Override
public int mul(int a, int b) {
int r = a * b;
return r;
}
@Override
public int div(int a, int b) {
int r = a / b;
return r;
}
}
class MyHandler implements InvocationHandler {
//关联
private Object target;
private Interceptor interceptor;
public MyHandler(Object target,Interceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//方法执行前的钩子函数
interceptor.before(target, method, args);
Object res = method.invoke(target, args);
//方法执行后的钩子函数
interceptor.after(target, method, args, res);
// 返回到代理对象的方法调用处
return res;
}
}
class MyProxy{
public Object getProxy(Object target, Interceptor interceptor){
//当前类的字节码获得当前类的类加载器
ClassLoader classLoader = MyProxy.class.getClassLoader();
//获取target所属的类,所实现的接口
Class<?>[] interfaces = target.getClass().getInterfaces();
//创建代理对象,需要传入三个参数
Object proxy = Proxy.newProxyInstance(classLoader,interfaces, new MyHandler(target,interceptor));
return proxy;
}
}
interface Interceptor {
//前置通知
void before(Object target, Method method, Object[] args);
//后置通知
void after(Object target, Method method, Object[] args, Object returnVal);
}
//----------------------------------------------------------------------------------------------------
//用户制作拦截器的实现类
class LogInterceptor implements Interceptor{
@Override
public void before(Object target, Method method, Object[] args) {
System.out.println(String.format("方法名为:%s,参数为:%s", method.getName(), Arrays.toString(args)));
}
@Override
public void after(Object target, Method method, Object[] args, Object returnVal) {
System.out.println(String.format("返回结果为:%s", returnVal.toString()));
}
}
public class AppTest {
public static void main(String[] args) {
ICalc calculator = new CalcImpl();
ICalc proxy = (ICalc) new MyProxy().getProxy(calculator,new LogInterceptor());
proxy.add(3, 2);
proxy.sub(3, 2);
proxy.mul(3, 2);
proxy.div(3, 2);
}
/**
* 方法名为:add,参数为:[3, 2]
* 返回结果为:5
* 方法名为:sub,参数为:[3, 2]
* 返回结果为:1
* 方法名为:mul,参数为:[3, 2]
* 返回结果为:6
* 方法名为:div,参数为:[3, 2]
* 返回结果为:1
*
* Process finished with exit code 0
*/
}
这样就简单了很多,应对不同的需求我们就去定制不同的代理类和拦截器,实现不同的需求
可是现在变化
又来了,客户有了新需求:
针对ICalc接口的日志功能,add方法使用中文日志,sub方法使用英文日志,mul方法和div方法不要日志。
这时只能用判断来解决了,针对不同方法有不同的日志。我们来实现一下
//用户制作拦截器的实现类
class LogInterceptor implements Interceptor{
@Override
public void before(Object target, Method method, Object[] args) {
if("add".equals(method.getName())){
System.out.println(String.format("方法名为:%s,参数为:%s", method.getName(), Arrays.toString(args)));
}else if("sub".equals(method.getName())){
System.out.println(String.format("methodName is:%s,parameter is:%s", method.getName(), Arrays.toString(args)));
}else{
System.out.println(method.getName()+Arrays.toString(args));
}
}
@Override
public void after(Object target, Method method, Object[] args, Object returnVal) {
if("add".equals(method.getName())){
System.out.println(String.format("返回结果为:%s", returnVal.toString()));
}else if("sub".equals(method.getName())){
System.out.println(String.format("result is:%s", returnVal.toString()));
}else{
System.out.println(returnVal.toString());
}
}
}
运行结果:
方法名为:add,参数为:[3, 2]
返回结果为:5
methodName is:sub,parameter is:[3, 2]
result is:1
mul[3, 2]
6
div[3, 2]
1
Process finished with exit code 0
可以看到,虽然做虽然满足了客户的需求,但是有很多的 if else ,感觉好像怪怪的!!!
仔细想想,这设计违反了什么设计原则呢?单一职责设计原则
那就拆分呗,设计原则不就是讲究一个分
字吗? 我们针对于四个方法,做四个拦截器,这里写两个作为演示
//用户制作拦截器的实现类
class addInterceptor implements Interceptor{
@Override
public void before(Object target, Method method, Object[] args) {
if("add".equals(method.getName())){
System.out.println(String.format("方法名为:%s,参数为:%s", method.getName(), Arrays.toString(args)));
}
}
@Override
public void after(Object target, Method method, Object[] args, Object returnVal) {
if("add".equals(method.getName())) {
System.out.println(String.format("返回结果为:%s", returnVal.toString()));
}
}
}
class subInterceptor implements Interceptor{
@Override
public void before(Object target, Method method, Object[] args) {
if("sub".equals(method.getName())){
System.out.println(String.format("methodName is:%s,parameter is:%s", method.getName(), Arrays.toString(args)));
}
}
@Override
public void after(Object target, Method method, Object[] args, Object returnVal) {
if("sub".equals(method.getName())){
System.out.println(String.format("result is:%s", returnVal.toString()));
}
}
}
客户端:
public class AppTest {
public static void main(String[] args) {
//calculator是目标对象
ICalc calculator = new CalcImpl();
//根据目标对象calculator,动态生成一个代理对象
ICalc proxy = (ICalc) new MyProxy().getProxy(calculator,new addInterceptor());
proxy.add(3, 2);
proxy.sub(3, 6);
}
/**
* 方法名为:add,参数为:[3, 2]
* 返回结果为:5
*
* Process finished with exit code 0
*/
}
但是问题又来了
现在,getProxy方法只能传addInterceptor 或者subInterceptor,不能同时用啊
有同学可能想到用可变参数解决这个问题,没错,是可以的;
但是换一种思路,既然能根据目标对象动态代理生成一个代理对象,那我是不是可以将这个代理对象再当成一个新的目标对象
动态代理一下?等等,感觉cpu要烧了!!!!
public class AppTest {
public static void main(String[] args) {
//calculator是目标对象
ICalc calculator = new CalcImpl();
//根据目标对象calculator,动态生成一个代理对象
ICalc proxy = (ICalc) new MyProxy().getProxy(calculator,new addInterceptor());
//我们把proxy这个代理对象,再当成一个新的目标对象
ICalc proxy2 = (ICalc) new MyProxy().getProxy(proxy,new subInterceptor());
//我们发现add方法和sub方法都能拦截
proxy2.add(3, 2);
proxy2.sub(3, 6);
}
/**
* 方法名为:add,参数为:[3, 2]
* 返回结果为:5
* methodName is:sub,parameter is:[3, 6]
* result is:-3
*
* Process finished with exit code 0
*/
}
简单理解就是套娃,贴一张图帮助理解
现在代码总没有问题了吧?是吗?那我说目前代码还有问题呢?
问题是:添加拦截器的顺序是逆向的,对用户不友好
解决方法:
public class AppTest {
public static void main(String[] args) {
//calculator是目标对象
ICalc calculator = new CalcImpl();
List<Interceptor> interceptors = new ArrayList<>();
interceptors.add(new addInterceptor());
interceptors.add(new subInterceptor());
for (int i =interceptors.size() - 1; i >=0; i--) {
Interceptor interceptor = interceptors.get(i);
calculator = (ICalc) MyProxy.getProxy(calculator, interceptor);
}
calculator.add(1,2);
calculator.sub(3,2);
}
/**
* 方法名为:add,参数为:[1, 2]
* 返回结果为:3
* methodName is:sub,parameter is:[3, 2]
* result is:1
*
* Process finished with exit code 0
*/
}
但是现在客户端代码很复杂了,对用户不友好呀
我们封装一下倒叙添加拦截器的逻辑
class MyProxy{
public static Object getProxy(Object target, Interceptor interceptor){
//当前类的字节码获得当前类的类加载器
ClassLoader classLoader = MyProxy.class.getClassLoader();
//获取target所属的类,所实现的接口
Class<?>[] interfaces = target.getClass().getInterfaces();
//创建代理对象,需要传入三个参数
Object proxy = Proxy.newProxyInstance(classLoader,interfaces, new MyHandler(target,interceptor));
return proxy;
}
//封装倒叙添加拦截器
public static Object getProxy2(Object target, List<Interceptor> interceptors){
for (int i =interceptors.size() - 1; i >=0; i--) {
Interceptor interceptor = interceptors.get(i);
target = (ICalc) MyProxy.getProxy(target, interceptor);
}
return target;
}
}
客户端代码:
public class AppTest {
public static void main(String[] args) {
//calculator是目标对象
ICalc calculator = new CalcImpl();
List<Interceptor> interceptors = new ArrayList<>();
interceptors.add(new addInterceptor());
interceptors.add(new subInterceptor());
ICalc proxy2 = (ICalc) MyProxy.getProxy2(new CalcImpl(), interceptors);
proxy2.add(1,2);
proxy2.sub(3,2);
}
/**
* 方法名为:add,参数为:[1, 2]
* 返回结果为:3
* methodName is:sub,parameter is:[3, 2]
* result is:1
*
* Process finished with exit code 0
*/
}
现在还有问题,以后,用户要添加拦截器,删除拦截器,必然要修改应用程序代码,要修改 List,这不合理鸭
用户应该是改配置而不是改代码,所以这个interceptors根本不用传
//封装倒叙添加拦截器
public static Object getProxy2(Object target) throws Exception{
//拦截器集合不是用户传进来的,是读取配置文件得到的,配置文件放在同一个包下
Properties prop = new Properties();
InputStream in = MyProxy.class.getResourceAsStream("myconfig.properties");
prop.load(in);
String str = prop.getProperty("interceptors");
String[] split = str.split(",");
List<Interceptor> interceptors = new ArrayList<>();
for (String hh : split) {
interceptors.add((Interceptor)Class.forName(hh).newInstance());
}
for (int i =interceptors.size() - 1; i >=0; i--) {
Interceptor interceptor = interceptors.get(i);
target = (ICalc) MyProxy.getProxy(target, interceptor);
}
return target;
}
客户端:
public static void main(String[] args) throws Exception {
ICalc proxy2 = (ICalc) MyProxy.getProxy2(new CalcImpl());
proxy2.add(1,2);
proxy2.sub(3,2);
}
客户端代码变得非常简洁
3、静态代理
业务场景:现在需要做一个图书解析器,解析一本书里面有多少个句子,多少个副词
package com.hh.demo.designpattern;
//图书解析器
class BookParser{
//接收一本书的内容,字符串的值,是很大的
private String content = "天下大事,分久必合,合久必分...!!";
public Integer numberOfSentence(){
//每次解析,都有很高的执行代价
return content.split("[.!?]").length;
}
public Integer numberOfVerb() throws InterruptedException {
//假设执行了很多逻辑;
Thread.sleep(1000);
return 80;
}
public Integer numberOfAdverb() throws InterruptedException {
//假设执行了很多逻辑;
Thread.sleep(1000);
return 220;
}
}
public class AppTest {
public static void main(String[] args) throws InterruptedException {
BookParser bp = new BookParser();
Integer a = bp.numberOfAdverb();
System.out.println("有"+ a + "个副词");
Integer a2 = bp.numberOfAdverb();
System.out.println("有"+ a2 + "个副词");
Integer a3 = bp.numberOfAdverb();
System.out.println("有"+ a3 + "个副词");
}
/**
* //每隔一秒出现一个结果
* 有220个副词
* 有220个副词
* 有220个副词
*
* Process finished with exit code 0
*/
}
现在有个问题,每解析一次就要花费1s, 这是极其不合理的;
我们可以做一个代理,每次调方法时进入代理,代理判断一下这个数字有没有统计过,如果统计过了,直接返回这个值,就不用去调用真实对象,如果没有统计过,就去调真实对象,然后返回值,并将这个值存到缓存中;
//图书解析器
class BookParser{
//接收一本书的内容,字符串的值,是很大的
private String content = "天下大事,分久必合,合久必分...!!";
public Integer numberOfSentence(){
//每次解析,都有很高的执行代价
return content.split("[.!?]").length;
}
public Integer numberOfVerb() throws InterruptedException {
//假设执行了很多逻辑;
Thread.sleep(1000);
return 80;
}
public Integer numberOfAdverb() throws InterruptedException {
//假设执行了很多逻辑;
Thread.sleep(1000);
return 220;
}
}
class BookParserProxy extends BookParser{
//因为没有定义接口,所以为了与真实对象有相同的方法,继承一下BookParser
private Integer numberOfSentence;
private Integer numberOfVerb;
private Integer numberOfAdverb;
@Override
public Integer numberOfSentence() {
if(numberOfSentence == null){
numberOfSentence = super.numberOfSentence();
}
return numberOfSentence;
}
@Override
public Integer numberOfVerb() throws InterruptedException {
if(numberOfVerb == null){
numberOfVerb = super.numberOfVerb();
}
return numberOfVerb;
}
@Override
public Integer numberOfAdverb() throws InterruptedException {
if(numberOfAdverb == null){
numberOfAdverb = super.numberOfAdverb();
}
return numberOfAdverb;
}
}
public class AppTest {
public static void main(String[] args) throws InterruptedException {
BookParser bp = new BookParserProxy();
Integer a = bp.numberOfAdverb();
System.out.println("有"+ a + "个副词");
Integer a2 = bp.numberOfAdverb();
System.out.println("有"+ a2 + "个副词");
}
/**
* //等待一秒后两个结果同时出现,说明什么,说明第二次没有调用真实方法
* 有220个副词
* 有220个副词
*
* Process finished with exit code 0
*/
}
这个就叫做静态代理,自己手写的代码,写死的代理类
而动态代理是运行时动态的生成字节码
4、代理模式的关键点
- 代理对象,一定与目标对象有相同的接口。这样才能做代理
- 代理对象中,一定有目标对象
- 代理对象,具有对目标对象的访问权限
其中上面的栗子中充分体现了前两点;第三点也可以体现,只需要改一下前置通知的返回值为boolean,然后做个判断就好啦
类似于现在要做一个权限验证的业务逻辑,再前置通知里面判断,返回true,才能调目标对象。
5、代理模式和适配器模式的比较
- 代理模式中,代理对象和它所包裹的目标对象,必须实现相同的接口;适配器模式种 ,适配器和它所包裹的对象不用实现相同的接口
- 代理模式中,代理对象可以控制它所包裹的目标对象的方法是否执行;适配器模式中,适配器总是调用目标对象的方法,无法控制