如何假裝成一個服務端開發(五)

目錄

如何假裝成一個服務端開發(一)java

如何假裝成一個服務端開發(二)spring

如何假裝成一個服務端開發(三) 編程

如何假裝成一個服務端開發(四)app

如何假裝成一個服務端開發(五)ide

 

代理模式

    在繼續以前最好先了解一下GoF中的代理模式,這裏再也不詳細展開。spring-boot

    代理模式的核心思想就是客戶不與最終對象交互,而和一種中間代理交互,中間代理再和最終對象交互。這樣的好處是,中間代理有機會干預整個交互流程。工具

使用Proxy.newProxyInstance

    java中有一種動態代理的東西,和上面代理模式中的靜態代理最大的不一樣點就在於在編譯階段代理類的.class文件是否已經產生。動態代理是在運行階段纔會產生一個代理類而且加載到classload中的。學習

    假設咱們有一個簡單的接口和實現測試

public interface HelloService {
    public void sayHello(String name);
}

public class HelloServiceImpl implements HelloService {

    @Override
    public void sayHello(String name) {
        if (name == null || name.trim() == "") {
            throw new RuntimeException ("parameter is null!!");
        }
        System.out.println("hello " + name);
    }

}

    如今我能夠在client中定義一個HelloServiceImpl進行調用,這是正常邏輯。可是如今我想要可以介入這個流程,因此這裏就須要放入代理模式,這沒毛病。this

    在定義一個攔截器接口和實現

public interface Interceptor {
    // 事前方法
    public boolean before();

    // 過後方法
    public void after();
    /**
     * 取代原有事件方法
     * @param invocation -- 回調參數,能夠經過它的 proceed 方法,回調原有事件
     * @return 原有事件返回對象
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    public Object around(Invocation invocation) 
        throws InvocationTargetException, IllegalAccessException;

    // 過後返回方法。事件沒有發生異常執行
    public void afterReturning();

    // 過後異常方法,當事件發生異常後執行
    public void afterThrowing();

    // 是否使用 around 方法取代原有方法
    boolean useAround();
}

public class MyInterceptor implements Interceptor {

    @Override
    public boolean before() {
        System.out.println("before ......");
        return true;
    }

    @Override
    public boolean useAround() {
        return true;
    }

    @Override
    public void after() {
        System.out.println("after ......");
    }

    @Override
    public Object around(Invocation invocation) 
           throws InvocationTargetException, IllegalAccessException {
        System.out.println("around before ......");
        Object obj = invocation.proceed();
        System.out.println("around after ......");
        return obj;
    }

    @Override
    public void afterReturning() {
        System.out.println("afterReturning......");

    }

    @Override
    public void afterThrowing() {
        System.out.println("afterThrowing......");
    }

}

    其中 Invocation是spring中的一個類,實際上並複雜,咱們看下源碼

public class Invocation {
    private Object[] params;
    private Method method;
    private Object target;

    public Invocation(Object target, Method method, Object[] params) {
        this.target = target;
        this.method = method;
        this.params = params;
    }
    // 反射方法
    public Object proceed() throws 
        InvocationTargetException, IllegalAccessException {
        return method.invoke(target, params);
    }

    /**** setter and getter ****/
}

    當調用proceed是,就經過反射方法直接調用target的指定方法。

    如今咱們來談談咱們的目的,咱們想要在調用HelloServiceImpl.sayHello的時候被攔截,而且按約定好的流程調用MyInterceptor中對應的方法。

    在上面整個代碼中,咱們發現缺失了代理模式中最重要的東西——代理。

   

public class ProxyBean implements InvocationHandler {
    private Object target = null;
    private Interceptor interceptor = null;

    /**
     * 綁定代理對象
     * @param target 被代理對象
     * @param interceptor 攔截器
     * @return 代理對象
     */
    public static Object getProxyBean(Object target, Interceptor interceptor) {
        ProxyBean proxyBean = new ProxyBean();
        // 保存被代理對象
        proxyBean.target = target;
        // 保存攔截器
        proxyBean.interceptor = interceptor;
        // 生成代理對象
        Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), 
             target.getClass().getInterfaces(),
                proxyBean);
        // 返回代理對象
        return proxy;
    }

    /**
     * 處理代理對象方法邏輯
     * @param proxy 代理對象
     * @param method 當前方法
     * @param args  運行參數
     * @return 方法調用結果
     * @throws Throwable 異常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)  {
        // 異常標識
        boolean exceptionFlag = false;
        Invocation invocation = new Invocation(target, method, args);
        Object retObj = null; 
        try {
            if (this.interceptor.before()) {
                retObj = this.interceptor.around(invocation);
            } else {
                retObj = method.invoke(target, args);
            }
        } catch (Exception ex) {
            // 產生異常
            exceptionFlag = true;
        }
        this.interceptor.after();
        if (exceptionFlag) {
            this.interceptor.afterThrowing();
        } else {
            this.interceptor.afterReturning();
            return retObj;
        }
        return null;
    }

}

    ProxyBean是一個工具方法,咱們固然還有其餘不少形式的寫法,可是不論怎麼寫,都繞不開關鍵的一步 

                                    newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)

    該方法的做用就是,在運行時生成一個繼承了interfaces類的類 A,而且使用loader進行加載,而後使用這個類A生成一個對象 a。當調用a中的方法時,都會調用h.invoke方法(InvocationHandler其實是個接口,只有一個invoke方法)。

    而後咱們使用下面代碼測試

private static void testProxy() {
    HelloService helloService = new HelloServiceImpl();
    // 按約定獲取proxy
    HelloService proxy = (HelloService) ProxyBean.getProxyBean(
        helloService, new MyInterceptor());
    proxy.sayHello("zhangsan");
    System.out.println("\n###############name is null!!#############\n");
    proxy.sayHello(null);
}

    就可以打印以下Log

before ......
around before ......
hello zhangsan
around after ......
after ......
afterReturning......

###############name is null!!#############

before ......
around before ......
after ......
afterThrowing......

    

AOP

    經過上面demo,咱們成功在運行的代碼中插入咱們想額外運行的方法(MyInterceptor)。Spring AOP的代碼核心實現就是使用和上面相同的動態代理模式。而所謂的AOP就是咱們在上面作的這件事情,將咱們的代碼(MyInterceptor),經過必定方法介入了原有流程(Client對target的調用)。

    下面咱們先來說解 AOP 術語。

  • 鏈接點(join point):對應的是具體被攔截的對象,由於 Spring 只能支持方法,因此被攔截的對象每每就是指特定的方法,例如,咱們前面提到的 HelloServiceImpl 的 sayHello 方法就是一個鏈接點,AOP 將經過動態代理技術把它織入對應的流程中。
  • 切點(point cut):有時候,咱們的切面不僅僅應用於單個方法,也多是多個類的不一樣方法,這時,能夠經過正則式和指示器的規則去定義,從而適配鏈接點。切點就是提供這樣一個功能的概念。
  • 通知(advice):就是按照約定的流程下的方法,分爲前置通知(before advice)、後置通知(after advice)、環繞通知(around advice)、過後返回通知(afterReturning advice)和異常通知(afterThrowing advice),它會根據約定織入流程中,須要弄明白它們在流程中的順序和運行的條件。
  • 目標對象(target):即被代理對象,例如,約定編程中的 HelloServiceImpl 實例就是一個目標對象,它被代理了。
  • 引入(introduction):是指引入新的類和其方法,加強現有 Bean 的功能。
  • 織入(weaving):它是一個經過動態代理技術,爲原有服務對象生成代理對象,而後將與切點定義匹配的鏈接點攔截,並按約定將各種通知織入約定流程的過程。
  • 切面(aspect):是一個能夠定義切點、各種通知和引入的內容,Spring AOP將經過它的信息來加強Bean的功能或者將對應的方法織入流程。

上述的描述仍是比較抽象的

                                      

Spring AOP編程

1.肯定鏈接點

    好比這裏仍是使用以前的demo,這裏就用 GameUser.printUserInfo來做爲鏈接點

public interface User{
    ...
    public void printUserInfo();
}

@Component("gameUser")
public class GameUser implements User , BeanNameAware,
        BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean {
    ....

    @Override
    public void printUserInfo() {
        log.info("I am Game User ,type is "+userInfo.getUserType());
    }
    ....
}

    

2.開發切面

    首先須要引入三方包

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

    

@Aspect
@Component
public class MyAspect {
    // before消息
    @Before("execution(* com.guardz.first_spring_boot.model.User.printUserInfo(..))")
    public void before() {
        System.out.println("before ......");
    }
    //不論執行對錯,after必定會被執行
    @After("execution(* com.guardz.first_spring_boot.model.User.printUserInfo(..))")
    public void after() {
        System.out.println("after ......");
    }


    @AfterReturning("execution(* com.guardz.first_spring_boot.model.User.printUserInfo(..))")
    public void afterReturning() {
        System.out.println("afterReturning ......");
    }

    @AfterThrowing("execution(* com.guardz.first_spring_boot.model.User.printUserInfo(..))")
    public void afterThrowing() {
        System.out.println("afterThrowing ......");
    }
}

    切面的開發首先須要使用@Aspect,另外切面自己也必須是一個能夠被注入的Bean,因此這裏使用了@Component(固然也能夠在容器配置中使用@Bean注入)。

    而後咱們會選擇咱們主要的消息,在消息發生時運行相應的方法。關於其中正則式的含義咱們稍晚再解釋,總的來講做用就是用來指定鏈接點的,例子中的意思就表示選擇了User中的printUserInfo的執行做爲鏈接點。

AOP注入

    這裏是筆者本身添加的流程,由於實在是遇到了一個坑,花了一成天的時間纔算弄明白。

    經過上面兩步,咱們已經具備了aop的全部物料。咱們嘗試測試。

@SpringBootApplication
public class FirstSpringBootApplication {

    private static Logger log = Logger.getLogger(FirstSpringBootApplication.class.getName());
    public static void main(String[] args) {
        SpringApplication.run(FirstSpringBootApplication.class, args);
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        User user = applicationContext.getBean(GameUser.class);
        user.printUserInfo();
        ((AnnotationConfigApplicationContext) applicationContext).close();
    }
}

    咱們發現注入aop沒有生效。爲何,由於這裏咱們使用了 AnnotationConfigApplicationContext 來建立一個AOP容器,可是這個容器默認是不支持aop的,因此咱們須要讓該容器支持aop,只須要在 AppConfig 類上添加註解進行配置便可。

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(value = "com.guardz.first_spring_boot.model")
public class AppConfig {
}

    運行,這個時候發現居然啓動報錯了

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.guardz.first_spring_boot.model.GameUser' available
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:343)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:335)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1101)
	at com.guardz.first_spring_boot.FirstSpringBootApplication.main(FirstSpringBootApplication.java:24)

    說是找不到 GameUser 這個bean,可是咱們看到咱們的GameUser已經使用了@Component註解,而且AppConfig的掃描路徑中確實包含了GameUser.

    這是什麼緣由呢?在Spring中有兩種實現動態代理的方法,一種是咱們上面介紹的Proxy,還有一種是CGLib。Proxy的代理模式是經過獲取目標類的接口(若是沒有實現接口,那麼會切換成CGLib的模式),而後動態實現接口。而CGLib的方式是生成一個該類的子類。默認狀況下,spring會使用Proxy的模式。咱們可使用  @EnableAspectJAutoProxy(proxyTargetClass=true)  強制所有使用CGLIB模式。

    一旦開啓了AOP模式,而且咱們目標類GameUser已經被代理。那麼咱們就沒法在容器中獲取GameUser對象,因此當調用getBean(GameUser.class)嘗試獲取GameUser對象的時,若是使用了CGLib模式(存在GameUser的子類,就是它的代理類),那麼可以正常運行。若是使用了Proxy模式,就會出現錯誤。

    因此總結來講,解決這個問題的方法是,使用 getBean(User.class) 或者使用  getBean("gameUser") .

    猜想另外,XXXApplication類調用main的時候回自動生成一個容器,這個容器的配置文件就是XXXApplication類自己。@SpringBootApplication 默認就包含了@ComponentScan,會掃描當前包以及它的子包,而且默認啓用了aop,因此若是咱們不使用 AnnotationConfigApplicationContext , 而直接新建類,使用@Autowired 注入gameUser,而且調用printUserInfo方法也可以運行

 

切點

    回到被咱們省略了的切面上面,咱們使用@Before這樣的註解定義了鏈接點,可是咱們發現我衝重複的謝了不少次鏈接點的正則式,這個時候就引入了切點@Pointcut . 修改切面文件

@Aspect
@Component
public class MyAspect {

    @Pointcut("execution(* com.guardz.first_spring_boot.model.User.printUserInfo(..))")
    public void pointCut(){

    }

    @Before("pointCut()")
    public void before() {
        System.out.println("before ......");
    }
    
    ..... 
}

    使用@Pointcut定義一個鏈接點,而後再@Before這些消息處直接使用這個鏈接點便可。

    而後咱們再來分析下這個正則式究竟是什麼意思

                    execution(* com.guardz.first_spring_boot.model.User.printUserInfo(..))

    execution() 表示在執行的時候,連接內部正則式匹配的方法

    * 表示任意返回類型

    com.guardz.first_spring_boot.model.User.printUserInfo 指定具體方法

    (..) 表示任意參數

    其實這裏的寫法仍是不少的,好比經常使用的@annotation()就表示當鏈接點帶有指定註解時進行攔截。這裏再也不詳述,能夠網上查找,後面的學習中應該也會逐步出現。

 

@Around

    咱們已經使用了@Befor @After等,可是還少了一個@Around通知,只有當咱們須要大幅修改原來代碼的時候纔會使用,不然儘可能不要使用。

    在原來的切面中添加

@Around("pointCut()")
public void around(ProceedingJoinPoint jp) throws Throwable {
    System.out.println("around before......");
    // 回調目標對象的原有方法
    jp.proceed();
    System.out.println("around after......");
}

    它擁有一個 ProceedingJoinPoint 類型的參數。這個參數的對象有一個 proceed 方法,經過這個方法能夠回調原有目標對象的方法。

    這裏有個問題,就是 around before和 before的調用順序,按照咱們這種寫法來的話,調用順序是這樣的

around before......
before ......
2018-12-29 15:29:13.245  INFO 96357 --- [           main] com.guardz.first_spring_boot.model.User  : I am Game User ,type is admin
around after......
after ......
afterReturning ......

    注意是先調用了 around before 後調用了 before。  但若是使用xml進行配置aop時,運行順序就變成了先調用before,在調用around before。    

    因此這個必定要注意,儘可能不要使用around.    

@DeclareParents

public interface DeclareUser{
    void doSomething();
}

public class DeclareUserImpl implements DeclareUser {
    @Override
    public void doSomething() {
        System.out.println("就是要出狂戰斧");
    }
}

而後修改MyAspect

@Aspect
public class MyAspect {
    @DeclareParents(
            value= "com.guardz.first_spring_boot.model.GameUser",
            defaultImpl=DeclareUserImpl.class)
    public DeclareUser declareUser;
    ....
}

    而後,當咱們經過getBean(User.class) 獲取到GameUser的代理對象以後,咱們能夠將它強行轉成DeclareUser類型,而且調用其中的doSomething方法。

    實現原理就是

                                

    咱們的代理對象會額外繼承咱們使用@DeclareParents 引入的接口。這個接口的實現方法就是defaultImpl制定的類。

    @DeclareParents 的value制定了須要被引入新接口的對象(注意這裏若是使用User對象會報錯,須要使用GameUser,由於是GameUser被代理了)。

    我能想到的一個用法是,讓DeclareUser接口繼承User接口(不繼承也能夠),而後再DeclareUserImpl中重寫GameUser當中的方法

public class DeclareUserImpl implements DeclareUser {
    ....
    @Override
    public void printUserInfo() {
        log.info("我就是要出狂戰斧");
    }

    @Override
    public void doSomething() {
        System.out.println("就是要出狂戰斧");
    }
}

    好比上面重寫printUserInfo方法,而後當咱們調用GameUser代理對象中的printUserInfo,實際上會調用到上面方法。可是很是不推薦這樣使用,由於邏輯隱藏太深,若是別人看你代碼可能會打你。

   

通知獲取參數

    首先咱們修改下User中的printUserInfo方法,簡單添加一個參數  void printUserInfo(String ext);     

    而後修改咱們的切面

@Aspect
public class MyAspect {

    ......
    //args(ext)表示獲取鏈接點方法中名稱爲ext的參數
    //JoinPoint jp 是可選的,無關緊要
    @Before("pointCut() && args(ext)")
    public void before(JoinPoint jp,String ext) {
        //能夠經過JoinPoint獲取全部參數
        Object[] args = jp.getArgs();
        System.out.println("before ...... " + ext);
    }
}

    咱們的before中已經帶上了ext的參數。

    另外@Around是麼有JoinPoint參數的,由於它自帶了ProceedingJoinPoint參數,能夠經過它獲取方法參數。

多個切面

    對於同一個鏈接點,咱們能夠註冊多個切面。多個切面之間的執行順序是隨機的,咱們能夠經過在切面上添加@Order()註解肯定運行順序,好比@Order(1) @Order(2) 數字越小,越早運行。

 

總結

    至此咱們基本瞭解了spring中的aop,而且學習了使用方法,可是這裏只介紹了使用註解的方式,aop也可以使用xml進行配置,可是既然已經知道了工做流程,相信若是遇到別人代碼使用xml進行了aop,咱們也可以經過查閱資料很快上手

 

參考&引用

    《深刻淺出 Spring Boot 2.X》

相關文章
相關標籤/搜索