Focus-1


  • 归档

  • 分类

  • 标签

  • 关于

  • 搜索

spring三个核心思想详解

发表于 2016-12-04 | 分类于 java , spring


Spring核心思想分三大类:控制反转(IOC),依赖注入(DI)和面向切面(AOP)

控制反转

通俗讲,控制权由应用代码中转到了外部容器,控制权的转移,是所谓反转。也就是说,正常我们都是新建对象,才可以调用对象。现在不需要了,交给容器来管理,我们只需要通过一些配置来完成把实体类交给容器这么个过程。这样可以减少代码量,简化开发的复杂度和耦合度。

这里,我要解释下几个概念:
1.控制反转只是一个概念,我理解为一种设计模式。
2.控制反转的主要形式有两种:依赖查找和依赖注入

依赖查找:容器提供回调接口和上下文条件给组件。EJB和Apache Avalon 都使用这种方式。这样一来,组件就必须使用容器提供的API来查找资源和协作对象,仅有的控制反转只体现在那些回调方法上(也就是上面所说的 类型1):容器将调用这些回调方法,从而让应用代码获得相关资源。
依赖注入:组件不做定位查询,只提供普通的Java方法让容器去决定依赖关系。容器全权负责的组件的装配,它会把符合依赖关系的对象通过JavaBean属性或者构造函数传递给需要的对象。通过JavaBean属性注射依赖关系的做法称为设值方法注入(Setter Injection);将依赖关系作为构造函数参数传入的做法称为构造器注入(Constructor Injection)。



依赖注入

上面也解释了这个概念,其实现方式有三种:属性注入(setter注入),构造器注入和自动装配。

方式一、属性注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.spring.demo02.entity;  
public class Programmer {  
  
    private String name;  
    private String sex;  
  
    // 在这里定义要依赖的computer属性,加上set方法  
    private Computer computer;  
  
    public String getName() {  
        return name;  
    }  
  
    public void setName(String name) {  
        this.name = name;  
    }  
  
    public String getSex() {  
        return sex;  
    }  
  
    public void setSex(String sex) {  
        this.sex = sex;  
    }  
  
    public Computer getComputer() {  
        return computer;  
    }  
  
    /** 
     * 加上Setter方法 
     * */  
    public void setComputer(Computer computer) {  
        this.computer = computer;  
    }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.spring.demo02.entity;  
public class Computer {

private String brand;
private String color;
private String size;

public void coding() {
System.out.println("Computer is coding!!!");
}

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}

public String getSize() {
return size;
}

public void setSize(String size) {
this.size = size;
}
}

看上面的代码,可以发现:

  • Programmer类里面,有3个属性,name,sex,computer,并且都有对应的getter、setter方法;

  • Computer类里面也有三个属性,分别是品牌、颜色、尺寸,也都有对应的getter、setter方法;

这只是第一步,在类里面声明属性并且实现set方法。

接下来,要写一个spring的xml配置文件,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>  
  
<beans xmlns="http://www.springframework.org/schema/beans"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xsi:schemaLocation="http://www.springframework.org/schema/beans  
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">  

  <bean id="programmer" class="com.spring.demo2.entity.Programmer">  
    <property name="name" value="小李"></property>
    <property name="sex" value="男"></property>
    <property name="computer" ref="computer"></property>  
  </bean>  

  <bean id="computer" class="com.spring.demo2.entity.Computer">  
    <property name="brand" value="hp"></property>  
    <property name="color" value="黑"></property>  
    <property name="size" value="14"></property>  
  </bean>
</beans>

解读一下这个xml文件:

  1. 声明一个bean,可以理解为实例化了一个对象。那这里实例化了两个对象(programmer和computer),各个属性都已经赋值上去。

  2. id为programmer的bean,其实就是Programmer类;通过给property赋值,Spring就会通过Programmer类的各个属性的set方法,逐一给Programmer的属性赋值。

  3. 在programmer里面,有一个属性是computer的,可以看到它属性值是 ref=”computer”,这就说明computer这个属性是个引用,这里ref后面的值其实就是指向另一个bean的id值,所以这里引用的是id为computer的bean。

以上,就是属性注入了。关键的是在类里面声明属性,写set方法,然后在xml里面配置bean和property的值。

方式二、构造器注入

构造器注入,顾名思义,就是在构造器里面注入依赖对象。那是怎么实现的呢?其实跟属性注入差不多,定义一个有参构造器,然后配置xml文件就行了。看代码:

1
2
3
4
5
6
7
8
9
10
11
package com.spring.demo03.entity;  
import com.spring.demo02.entity.Computer;

public class Programmer {

private Computer computer;

public Programmer(Computer computer){
this.computer = computer;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.spring.demo03.entity;  

public class Computer {

private String brand;
private String color;
private String size;

public Computer(String brand, String color, String size) {
this.brand = brand;
this.color = color;
this.size = size;
}
}

上面两个类都有一个有参的构造器,接下来,在xml里面配置这两个bean,然后再配置构造器的参数值就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>  
  
<beans xmlns="http://www.springframework.org/schema/beans"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xsi:schemaLocation="http://www.springframework.org/schema/beans  
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">  
      
  <bean id="programmer" class="com.spring.demo3.entity.Programmer">  
    <constructor-arg ref="computer"></constructor-arg>  
  </bean>  

  <!-- 构造器里面没有name字段,只有value,是根据构造器的方法参数顺序来定义的 -->  
  <bean id="computer" class="com.spring.demo3.entity.Computer">  
    <constructor-arg value="联想"></constructor-arg>  
    <constructor-arg value="红色"></constructor-arg>  
    <constructor-arg value="15.6寸"></constructor-arg>  
  </bean>  
    
</beans>

方式三:自动装配

1
2
3
4
5
6
7
8
9
10
11
package com.spring.demo04.entity;  
  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Component;  
  
@Component  
public class Programmer {

    @Autowired  
    Computer computer;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.spring.demo04.entity;  
  
import org.springframework.stereotype.Component;  
  
@Component  
public class Computer {  
    private String brand;  
    private String color;  
    private String size;
 
    public String getBrand() {  
        return brand;  
    }  
  
    public void setBrand(String brand) {  
        this.brand = brand;  
    }  
  
    public String getColor() {  
        return color;  
    }  
  
    public void setColor(String color) {  
        this.color = color;  
    }  
  
    public String getSize() {  
        return size;  
    }  
  
    public void setSize(String size) {  
        this.size = size;  
    }  
}
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>  
  
<beans xmlns="http://www.springframework.org/schema/beans"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xmlns:context="http://www.springframework.org/schema/context"  
    xsi:schemaLocation="http://www.springframework.org/schema/beans  
    http://www.springframework.org/schema/context   
    http://www.springframework.org/schema/context/spring-context-3.0.xsd   
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">  

    <context:component-scan base-pakage="com.spring.demo04">  
</beans>

关键点:

  1. 在类前面加注解:@Component;
  2. 在需要注入的类里面加注解:@Autowired,这样xml里面的自动扫描就会扫描到这些加了注解的类和属性,在实例化bean的时候,Spring容器会把加了@Component的类实例化;在实际运行时,会给加了@Autowired的属性注入对应的实例。@Autowired方式是通过反射来设置属性值的;

具体参考:https://blog.csdn.net/wenluoxicheng/article/details/73608657

面向切面

面向切面是一个概念,通常用来为许多其他的类提供相同的服务,而且是固定的服务,是独立的。提升了代码的复用性,减少了代码的耦合度,减轻程序员的工作负担,把程序的重心放在了核心的逻辑上。

它是可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。

应用场景有日志记录,性能统计,安全控制,事务处理,异常处理等。

Spring AOP

发表于 2016-12-01 | 分类于 java , spring

AOP基础知识

概念和术语

连接点:应用程序执行期间明确定义的一个点。eg:方法调用、方法调用本身、类初始化、对象实例化;

通知(Advice):在特定连接点执行的代码就是通知。通知类型举例:前置通知(在连接点之前执行),后置通知(在连接点之后执行);

切入点(Pointcut):用于定义何时执行的 连接点集合

切面(Advisor):封装 类中 通知和切入点的集合;

织入:在适当的位置 将切面插入到 应用程序 代码中的过程;编译时AOP:在生成时完成;运行时AOP:在运行态执行;AspectJ还支持另一种加载时织入(LTW)的植入机制;拦截底层的JVM类加载器,在加载字节码时织入;

目标对象(target):由AOP进程修改的对象 称为 目标对象;通常也被称为 被通知对象;

引入:通过引入其他方法 来改变 对象结构 的过程。可以通过引入 AOP来使任何对象实现特定的接口,而无需对象的类显式实现该接口;



AOP类型

静态AOP

织入过程 是 应用程序生成的一个步骤;

修改 实际的字节码 来实现 AOP织入过程,最终结果是Java字节码,并且运行时无需特殊技巧确定应该何时执行;

缺点:对切面做的任何修改都需要 重新编译整个程序;

AspectJ的编译时织入 就是 静态AOP;

动态AOP

织入过程 在运行时 动态执行;

缺点:性能不如静态AOP;

优点:可以轻松修改 应用程序的整个切面集,而无需重新编译主应用程序代码;

选择AOP类型

静态AOP和动态AOP各有优点

静态AOP: 实现时间长,倾向于实现更多功能丰富的实现;具有更多的可用连接点;而且如果性能很重要,就倾向于静态AOP;

如果需要SpringAOP未实现的功能 就需要 AspectJ;



Spring AOP示例

Spring AOP实现两个逻辑部分:

1、AOP内核:完全解耦的纯编程 AOP功能;

2、使 AOP 更易于使用的 一组框架服务;

AOP Alliance

许多开源项目共同定制的 一组AOP标准接口。

Spring使用了AOP Alliance 接口

官网:http://aopalliance.sourceforge.net/

AOP Alliance示例

1、pom.xml中引入jar包

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.6.RELEASE</version>
</dependency>

2、定义目标对象

1
2
3
4
5
public class Agent {
public void speak() {
System.out.print("Bond");
}
}

3、定义增强类 AgentDecorator

目标:打印 James Bond!

1
2
3
4
5
6
7
8
9
10
11
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class AgentDecorator implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.print("James ");
Object ret = invocation.proceed();
System.out.println("!");
return ret;
}
}

实现 AOP Alliance 标准接口 MethodInterceptor,围绕 invocation.proceed() 实现 环绕通知(around advice);

4、将invocation.proceed()方法 织入 目标对象 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.aop.framework.ProxyFactory;

public class AgentAOPDemo {
public static void main(String[] args) {
Agent target = new Agent();
target.speak();
System.out.println();

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.addAdvice(new AgentDecorator());
proxyFactory.setTarget(target);
Agent proxy = ((Agent) proxyFactory.getProxy());
proxy.speak();
}
}

运行结果:

1
2
Bond
James Bond!


Spring AOP架构

Spring AOP的核心架构 基于 代理。

想要创建一个类的被通知实例时,必须使用ProxyFactory创建该类的代理实例;

大多数情况下,我们可以使用声明式AOP配置(ProxyFactoryBean、AOP名称空间和@AspectJ样式注解)来完成代理的创建;

但我们需要了解代理的创建过程;因此,首先延时代理创建的编程方法,再讨论 Spring的声明式AOP配置;

运行时,Spring会分析为 ApplicationContext中bean定义的横切关注点,并动态生成代理 bean(封装了底层的目标bean)。不会直接调用目标bean,而是将调用者注入代理bean。然后代理bean分析运行条件,并相应的织入适当的通知。

Spring中的两种代理实现:JDK动态代理、CGLIB代理

Spring中的连接点

Spring只支持一种连接点类型:方法调用;

如果需要其他连接点:则可以使用Spring和AspectJ;

Spring中的切面

在Spring AOP中,切面由实现了 Advisor 接口的类的实例表示。

Advisor有两个子接口:PointcutAdvisor 和 IntroductionAdvisor。

关于ProxyFactory类

ProxyFactory 控制 SpringAOP 中织入和代理创建过程;

1
Agent proxy = ((Agent) proxyFactory.getProxy());

ProxyFactory将代理创建委托给 DefaultProxyFactory的一个实例,继而又委托给 ObjenesisCglibAopProxy 或 JdkDynamicAopProxy

1
proxyFactory.addAdvice(new AgentDecorator());

addAdvice将传入的通知封装到 DefaultPointcutAdvisor的一个实例中,并使用默认的方法进行配置

ProxyFactory可以创建多个代理,每个代理都有不同的切面

1
2
3
4
proxyFactory.addAdvice();
proxyFactory.removeAdvice();
proxyFactory.addAdvisor();
proxyFactory.removeAdvisor();

在Spring中创建通知

Spring中支持6中通知

通知名称 接口 描述
前置通知 org.springframework.aop.MethodBeforeAdvice 在连接点之前执行
后置返回通知 org.springframework.aop.AfterReturningAdvice 在连接点return之后 执行
后置通知 org.springframework.aop.AfterAdvice 连接点执行完成之后执行
环绕通知 org.aopalliance.intercept.MethodInterceptor 在连接点前后都执行
异常通知 org.springframework.aop.ThrowsAdvice 在异常执行以后执行,捕获特定异常
引入通知 org.springframework.aop.IntroductionAdvisor 将引入建模为特殊类型的拦截器

通知(Advice)的接口

通知(Advice)和顾问(Advisor)的区别在于:Advisor可以携带相关切入点的通知,从而更细致的控制在哪个连接点上拦截通知。

Advice的层次结构图:

创建前置通知(MethodBeforeAdvice)

创建目标对象:

1
2
3
4
5
6
7
8
public interface Singer {
void sing();
}
public class Guitarist implements Singer {
public void sing() {
System.out.println("You're gonna live forever in me.");
}
}

创建前置通知:

1
2
3
4
5
6
7
8
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;

public class SimpleBeforeAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("Before '" + method.getName() + "' , tune guitar.");
}
}

设置切入点并运行示例:

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.aop.framework.ProxyFactory;

public class SimpleBeforeAdviceDemo {
public static void main(String[] args) {
Guitarist johnMayer = new Guitarist();
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.addAdvice(new SimpleBeforeAdvice());
proxyFactory.setTarget(johnMayer);
Guitarist proxy = ((Guitarist) proxyFactory.getProxy());
proxy.sing();
}
}

运行结果:

1
2
Before 'sing' , tune guitar.
You're gonna live forever in me.

创建后置返回通知

1
2
3
4
5
6
7
8
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;

public class SimpleAfterReturningAdvice implements AfterReturningAdvice {
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("After "+ method.getName() +" return , return value: " + returnValue);
}
}

创建异常通知(ThrowsAdvice)

ThrowsAdvice内置四个接口,通过反射调用:

1
2
3
4
public void afterThrowing(Exception ex)
public void afterThrowing(RemoteException)
public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import org.springframework.aop.ThrowsAdvice;
import org.springframework.aop.framework.ProxyFactory;
import java.lang.reflect.Method;

public class SimpleThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(Exception ex) throws Throwable {
System.out.println("*****");
System.out.println("Caught: " + ex.getClass().getName());
System.out.println("Message: " + ex.getMessage());
}
public void afterThrowing(Method method, Object[] args, Object target, Exception ex) {
System.out.println("*****");
System.out.println("Caught2: " + ex.getClass().getName());
System.out.println("Message2: " + ex.getMessage());
}

public static void main(String[] args) {
ErrBean errBean = new ErrBean();
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(errBean);
proxyFactory.addAdvice(new SimpleThrowsAdvice());
ErrBean proxy = ((ErrBean) proxyFactory.getProxy());
try {
proxy.errMethod();
} catch (Exception e) {
e.printStackTrace();
}
}
}

class ErrBean {
public void errMethod() throws Exception {
throw new Exception("My Exception");
}
}

执行结果:

可以看到只执行了第二个方法,通过Cglib代理;



使用顾问(Advisor)和切入点(Pointcut)

避免硬编码 将方法检查 放入通知,尽可能使用切入点来控制通知目标上方法的适用性;

如果通知跟目标 之间有 目标关联性,则在通知内部检查也可以;

Pointcut接口

1
2
3
4
5
6
7
package org.springframework.aop;

public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
Pointcut TRUE = TruePointcut.INSTANCE;
}
1
2
3
4
5
6
package org.springframework.aop;
// 判断 Pointcut是否适用于特定的方法
public interface ClassFilter {
boolean matches(Class<?> clazz);
ClassFilter TRUE = TrueClassFilter.INSTANCE;
}
1
2
3
4
5
6
7
8
9
package org.springframework.aop;
import java.lang.reflect.Method;

public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
boolean isRuntime();
boolean matches(Method method, Class<?> targetClass, Object[] args);
MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
}

MethodMatcher 分为 静态MethodMatcher 和 动态 MethodMatcher;

Spring 通过 isRuntime() 判断 MethodMatcher是 静态(false),还是动态(true);

1、静态切入点:

Spring 针对目标上的每个方法,调用一次MethodMatcher的matches(Method, Class<?> )方法,并缓存返回值;

2、动态切入点:

Spring先调用 matches(Method, Class<?> )方法进行静态检查,返回true时,再调用 matches(Method, Class<?>, Object[]) 对每个参数进一步检查;

比较:

静态Pointcut 执行的比较好,动态Pointcut更灵活;推荐尽量使用静态Pointcut;

使用动态Pointcut 可以 避免 不必要的通知调用;通知调用对性能影响很大;

可用的Pointcut实现

4.0开始Spring提供了八个Pointcut接口的实现:2个用作创建静态、动态切入点的抽象类,6个具体类;

每个具体类可以完成的操作:

  • 一起构成多个切入点

  • 处理控制流切入点

  • 执行简单的基于名称的匹配

  • 使用正则表达式定义切入点

  • 使用AspectJ定义切入点

  • 定义在类或者方法级别查找特定注解的切入点

实现类 描述
org.springframework.aop.support.annotation.
AnnotationMatchingPointcut
在类或方法上查找指定Java注解
org.springframework.aop.aspectj.
AspectJExpressionPointcut
使用AspectJ植入器以AspectJ语法评估切入点表达式
org.springframework.aop.support.
ComposablePointcut
ComposablePointcut类使用诸如union()和intersection()等操作组合两个或更多的切入点
org.springframework.aop.support.
ControlFlowPointcut
ControlFlowPointcut是一种特殊的切入点,他们匹配另一个方法的控制流中的所有方法,即任何作为另一个方法的结果而直接或间接调用的方法
org.springframework.aop.support.
DynamicMethodMatcherPointcut
构建动态切入点的基类
org.springframework.aop.support.
JdkRegexpMethodPointcut
正则表达式支持定义切入点
org.springframework.aop.support.
NameMatchMethodPointcut
创建一个切入点,对方法名称列表执行简单匹配
org.springframework.aop.support.
StaticMethodMatcherPointcut
构建静态切入点的基础

使用DefaultPointcutAdvisor

使用Pointcut之前,必须先创建 Advisor接口的实例,具体的说是创建一个PointcutAdvisor接口;

Advisor是Spring中某个切面的表示,它是 通知 和 切入点 的 结合体;

DefaultPointcutAdvisor 将 点那个Pointcut 和 Advice 相关联;

1、使用 StaticMethodMatcherPointcut 创建静态切入点

思路:创建两个目标对象类GoodGuitarist和GreatGuitarist,通过DefaultPointcutAdvisor创建这两个类的代理,但通知 只应用到GoodGuitarist;

创建两个目标对象类:

1
2
3
4
5
6
7
8
9
10
public class GoodGuitarist implements Singer {
public void sing() {
System.out.println("I am GoodGuitarist");
}
}
public class GreatGuitarist implements Singer {
public void sing() {
System.out.println("I am GreatGuitarist");
}
}

创建静态切入点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.aop.ClassFilter;
import org.springframework.aop.support.StaticMethodMatcherPointcut;

import java.lang.reflect.Method;

public class SimpleStaticPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method method, Class<?> cls) {
return "sing".equals(method.getName()); // 只把通知应用到sing()方法
}

@Override
public ClassFilter getClassFilter() {
return new ClassFilter() {
@Override
public boolean matches(Class<?> clazz) {
return clazz == GoodGuitarist.class; // 只将通知应用于GoodGuitarist
}
};
}
}

创建通知:

1
2
3
4
5
6
7
8
9
10
11
12
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class SimpleAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println(">> Invoke: " + invocation.getMethod().getName());
Object ret = invocation.proceed();
System.out.println("<< Invoke Done!");
return ret;
}
}

使用DefaultPointcutAdvisor组织Pointcut和Advice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import com.learn.aop.Singer;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Advisor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class StaticPointcutDemo {
public static void main(String[] args) {
GoodGuitarist good = new GoodGuitarist();
GreatGuitarist great = new GreatGuitarist();

Singer goodProxy, greatProxy;
Pointcut pointcut = new SimpleStaticPointcut();
Advice advice = new SimpleAdvice();
Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.addAdvisor(advisor);
proxyFactory.setTarget(good);
goodProxy = (Singer) proxyFactory.getProxy();

proxyFactory = new ProxyFactory();
proxyFactory.addAdvisor(advisor);
proxyFactory.setTarget(great);
greatProxy = ((Singer) proxyFactory.getProxy());

goodProxy.sing();
System.out.println();
greatProxy.sing();
}
}

运行结果:

1
2
3
4
5
>> Invoke: sing
I am GoodGuitarist
<< Invoke Done!

I am GreatGuitarist

可以看到只有 GoodGuitarist的sing方法被通知,GreatGuitarist没有被通知;

2、使用DynamicMethodMatcherPointcut创建动态切入点

在本例中,只想通知foo()方法,而且 只有在 int参数 不等于 100 时,才通知该方法

创建目标对象:

1
2
3
4
5
6
7
8
public class SampleBean {
public void foo(int x) {
System.out.println("invoke foo() with: " + x);
}
public void bar() {
System.out.println("invoke bar() ");
}
}

创建针对foo的切入点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.springframework.aop.ClassFilter;
import org.springframework.aop.support.DynamicMethodMatcherPointcut;
import java.lang.reflect.Method;

public class SimpleDynamicPointcut extends DynamicMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> targetClass) {
System.out.println("static check for " + method.getName() + "(), calss: " + targetClass.getName());
return "foo".equals(method.getName()); // 针对foo()方法
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object[] args) {
System.out.println("dynamic check for " + method.getName() + "(), calss: " + targetClass.getName());
int x = ((Integer) args[0]).intValue();
return x != 100; // 判断参数是否 等于100
}
@Override
public ClassFilter getClassFilter() {
return new ClassFilter() {
@Override
public boolean matches(Class<?> clazz) {
return clazz == SampleBean.class; // 判断类是否 SampleBean
}
};
}
}

创建Advisor组织切入点和通知,调用代理对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.learn.aop.pointcut.staticp.SimpleAdvice;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class DynamicPointcutDemo {
public static void main(String[] args) {
SampleBean target = new SampleBean();

Advisor advisor = new DefaultPointcutAdvisor(new SimpleDynamicPointcut(), new SimpleAdvice());

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisor(advisor);
SampleBean proxy = ((SampleBean) proxyFactory.getProxy());

proxy.foo(1);
proxy.foo(10);
proxy.foo(100);
proxy.bar();
proxy.bar();
proxy.bar();
}
}

运行结果:

可以看到只有两个foo()被通知到,x=100时没有被通知;bar()没有被通知到;

foo进行了两次静态检查,一次在初始阶段,一次在被调用时;

3、使用简单名称匹配

无需考虑方法签名,只根据方法名匹配;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Guitar {
public String play() {
return "G C GC AM D7";
}
}
public class GrammyGuitarist implements Singer{
@Override
public void sing() {
System.out.println("sing()");
}
public void sing(Guitar guitar) {
System.out.println("play(): " + guitar.play());
}
public void rest() {
System.out.println("zzz~~");
}
public void talk() {
System.out.println("talk()");
}
}

使用NameMatchMethodPointcut匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import com.learn.aop.pointcut.staticp.SimpleAdvice;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;

public class NamePointcutDemo {
public static void main(String[] args) {
GrammyGuitarist johnMayer = new GrammyGuitarist();

NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.addMethodName("sing");
pointcut.addMethodName("rest");

Advisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleAdvice());
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(johnMayer);
proxyFactory.addAdvisor(advisor);

GrammyGuitarist proxy = ((GrammyGuitarist) proxyFactory.getProxy());
proxy.sing();
proxy.sing(new Guitar());
proxy.rest();
proxy.talk();
}
}

运行结果:

4、使用正则表达式创建切入点

创建目标对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.learn.aop.Singer;

public class Guitarist implements Singer {
@Override
public void sing() {
System.out.println("sing()");
}
public void sing2() {
System.out.println("sing2()");
}
public void rest() {
System.out.println("rest()");
}
}

创建Advisor组织切入点和通知,并调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.learn.aop.pointcut.staticp.SimpleAdvice;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.JdkRegexpMethodPointcut;

public class RegexPointcutDemo {
public static void main(String[] args) {
Guitarist target = new Guitarist();

JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPattern(".*sing.*");
Advisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleAdvice());

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.addAdvisor(advisor);
proxyFactory.setTarget(target);

Guitarist proxy = (Guitarist) proxyFactory.getProxy();
proxy.sing();
proxy.sing2();
proxy.rest();
}
}

运行结果:

5、使用 AspectJ切入点表达式 创建切入点

AspectJ切入点表达式

在pom.xml中添加:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.4</version>
</dependency>

使用 AspectJ表达式 实现 JDK正则表达式 相同的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.learn.aop.pointcut.regex.Guitarist;
import com.learn.aop.pointcut.staticp.SimpleAdvice;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class AspectjexpPointcutDemo {
public static void main(String[] args) {
Guitarist target = new Guitarist();
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* sing*(..))");
Advisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleAdvice());

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisor(advisor);

Guitarist proxy = ((Guitarist) proxyFactory.getProxy());
proxy.sing();
proxy.sing2();
proxy.rest();
}
}

运行结果:

6、创建注解匹配切入点

基于注解定义切入点AnnotationMatchingPointcut;

定义注解:

1
2
3
4
5
6
7
8
9
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AdviceRequired {
}

在方法上使用注解:

1
2
3
4
5
6
7
8
9
10
11
12
public class Guitarist implements Singer {
public void sing() {
System.out.println("sing()");
}
@AdviceRequired
public void sing(Guitar guitar) {
System.out.println("play: " + guitar.play());
}
public void rest() {
System.out.println("rest()");
}
}

对注解类织入切面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.learn.aop.pointcut.name.Guitar;
import com.learn.aop.pointcut.staticp.SimpleAdvice;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;

public class AnnotationPointcutDemo {
public static void main(String[] args) {
Guitarist target = new Guitarist();

AnnotationMatchingPointcut pointcut = AnnotationMatchingPointcut
.forMethodAnnotation(AdviceRequired.class);
Advisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleAdvice());

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisor(advisor);

Guitarist proxy = ((Guitarist) proxyFactory.getProxy());
proxy.sing(new Guitar());
proxy.rest();
}
}

运行结果:



了解代理ProxyFactory

到目前为止,只是粗略的了解了一下 ProxyFactory的使用;

代理的核心:拦截方法调用,必要时 执行适用于特定方法的通知链;

通知的管理和调用 独立于代理,由Spring AOP框架管理。而代理主要负责拦截对方法的调用,并将它们根据需要传递给AOP框架,以便应用通知。

Spring有两种代理:JDK代理、CGLIB代理;

JDK动态代理

只能生成接口的代理,不能生成类的代理;

如何处理特定方法调用的决策 都在 运行时 做出;

无法区分 被通知方法 和 未被通知方法;

1
2
3
4
5
6
7
8
9
10
public static void runJdkTests(Advisor advisor, SimpleBean target) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisor(advisor);
proxyFactory.setInterfaces(SimpleBean.class);

SimpleBean proxy = ((SimpleBean) proxyFactory.getProxy());
System.out.println(">>> Running JDK Tests");
test(proxy);
}

CGLIB代理

CGLIB会为每个代理 动态生成新类的字节码,并尽可能重用已生成的类;

所生成的类 是 目标对象target类的子类;

每次创建CGLIB代理时,CGLIB会询问Spring如何处理每个方法;

由于CGLIB生成实际的字节码,因此在处理方法上很灵活;

CGLIB生成固定通知链以后,可以减少执行通知链的运行时间开销;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void runCglibTests(Advisor advisor, SimpleBean target) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisor(advisor);
proxyFactory.setProxyTargetClass(true);

SimpleBean proxy = ((SimpleBean) proxyFactory.getProxy());
System.out.println(">>> Running CGLIB (Standard) Tests");
test(proxy);
}
public static void runCglibFrozenTests(Advisor advisor, SimpleBean target) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisor(advisor);
proxyFactory.setProxyTargetClass(true);
proxyFactory.setFrozen(true);

SimpleBean proxy = ((SimpleBean) proxyFactory.getProxy());
System.out.println(">>> Running CGLIB (Frozen) Tests");
test(proxy);
}

比较代理性能

定义Target:

1
2
3
4
public interface SimpleBean {
void advised();
void unadvised();
}
1
2
3
4
5
6
7
8
9
public class DefaultSimpleBean implements SimpleBean {
private long dummy = 0;
public void advised() {
dummy = System.currentTimeMillis();
}
public void unadvised() {
dummy = System.currentTimeMillis();
}
}

定义通知:

1
2
3
4
5
6
7
8
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;

public class NoOpBeforeAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] args, Object target) throws Throwable {
// 什么都不做
}
}

定义切入点:

1
2
3
4
5
6
7
8
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import java.lang.reflect.Method;

public class TestPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method method, Class<?> targetClass) {
return "advise".equals(method.getName());
}
}

分别调用JDK代理,CGLIB代理,CGLIB Frozen代理,比较他们的性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class ProxyPerfTest {
public static void main(String[] args) {
SimpleBean target = new DefaultSimpleBean();

Advisor advisor = new DefaultPointcutAdvisor(new TestPointcut(), new NoOpBeforeAdvice());

runCglibTests(advisor, target);
runCglibFrozenTests(advisor, target);
runJdkTests(advisor, target);
}

public static void runCglibTests(Advisor advisor, SimpleBean target) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisor(advisor);
proxyFactory.setProxyTargetClass(true);

SimpleBean proxy = ((SimpleBean) proxyFactory.getProxy());
System.out.println(">>> Running CGLIB (Standard) Tests");
test(proxy);
}
public static void runCglibFrozenTests(Advisor advisor, SimpleBean target) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisor(advisor);
proxyFactory.setProxyTargetClass(true);
proxyFactory.setFrozen(true);

SimpleBean proxy = ((SimpleBean) proxyFactory.getProxy());
System.out.println(">>> Running CGLIB (Frozen) Tests");
test(proxy);
}
public static void runJdkTests(Advisor advisor, SimpleBean target) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisor(advisor);
proxyFactory.setInterfaces(SimpleBean.class);

SimpleBean proxy = ((SimpleBean) proxyFactory.getProxy());
System.out.println(">>> Running JDK Tests");
test(proxy);
}

private static void test(SimpleBean bean) {
long before = 0;
long after = 0;
System.out.println("Testing advised() Method");
before = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
bean.advised();
}
after = System.currentTimeMillis();
System.out.println("Took " + (after-before) + " ms");

System.out.println("Testing unadvised() Method");
before = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
bean.unadvised();
}
after = System.currentTimeMillis();
System.out.println("Took " + (after-before) + " ms");

System.out.println("Test equals() method:");
before = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
bean.equals(bean);
}
after = System.currentTimeMillis();
System.out.println("Took " + (after-before) + " ms");

System.out.println("Test hashCode() method:");
before = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
bean.hashCode();
}
after = System.currentTimeMillis();
System.out.println("Took " + (after-before) + " ms");

Advised advised = ((Advised) bean);
System.out.println("Test advised.getTargetClass() method:");
before = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
advised.getTargetClass();
}
after = System.currentTimeMillis();
System.out.println("Took " + (after-before) + " ms");

System.out.println("<<<\n");
}
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
>>> Running CGLIB (Standard) Tests
Testing advised() Method
Took 84 ms
Testing unadvised() Method
Took 57 ms
Test equals() method:
Took 12 ms
Test hashCode() method:
Took 21 ms
Test advised.getTargetClass() method:
Took 9 ms
<<<

>>> Running CGLIB (Frozen) Tests
Testing advised() Method
Took 16 ms
Testing unadvised() Method
Took 51 ms
Test equals() method:
Took 10 ms
Test hashCode() method:
Took 19 ms
Test advised.getTargetClass() method:
Took 10 ms
<<<

>>> Running JDK Tests
Testing advised() Method
Took 130 ms
Testing unadvised() Method
Took 110 ms
Test equals() method:
Took 161 ms
Test hashCode() method:
Took 94 ms
Test advised.getTargetClass() method:
Took 68 ms
<<<

可以看到JDK代理的性能最低,其次,CGLIB Frozen的性能要明显好于 CGLIB;

选择要使用的代理

CGLIB 可以代理类和接口,而JDK只能代理 接口;

CGLIB Frozen模式性能明显好于 JDK代理和CGLIB代理;

如果要使用CGGLIB代理接口,必须使用proxyFactory.setOptimize(true);方法将ProxyFactory的optimize标志设置为true;



切入点的高级使用

前面讲了6个Pointcut的使用,如果如下更大的灵活性,可以使用 ComposablePointcut 和 ControlFlowPointcut

使用控制流切入点ControlFlowPointcut

定义target:

1
2
3
4
5
public class TestBean {
public void foo() {
System.out.println("foo()");
}
}

定义通知:

1
2
3
4
5
6
7
8
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;

public class SimpleBeforeAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("Before method: " + method);
}
}

使用控制流ControlFlowPointcut:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.springframework.aop.Advisor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.ControlFlowPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class ControlFlowDemo {
public static void main(String[] args) {
ControlFlowDemo ex = new ControlFlowDemo();
ex.run();
}
public void run() {
TestBean target = new TestBean();
Pointcut pointcut = new ControlFlowPointcut(ControlFlowDemo.class, "test");
Advisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleBeforeAdvice());

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisor(advisor);

TestBean proxy = ((TestBean) proxyFactory.getProxy());
System.out.println("\tTrying normal invoke");
proxy.foo();
System.out.println("\n\tTrying under ControlFlowDemo.test()");
test(proxy);
}
private void test(TestBean bean) {
bean.foo();
}
}

运行结果:

1
2
3
4
5
6
	Trying normal invoke
foo()

Trying under ControlFlowDemo.test()
Before method: public void com.learn.aop.pointcut.controlflow.TestBean.foo()
foo()

关键代码:

1
Pointcut pointcut = new ControlFlowPointcut(ControlFlowDemo.class, "test");

表示 为ControlFlowDemo类的test()方法 创建了一个 ControlFlowPointcut实例, 切入从ControlFlowDemo.test() 调用 target 的所有方法;

target 在 test()之外调用时,并不会被通知;只有在test()内调用时,才会被通知;

缺点:在其他切入点上使用 ControlFlowPointcut 会大大降低性能

使用组合切入点ComposablePointcut

在前面的实例中,仅为每个Advisor使用了一个切入点。在大多数情况下,这通常是足够的,但有时需要多个Pointcut组合在一起 共同实现目标;

ComposablePointcut 支持两种方法:union() 《等价于 “or”运算》和 intersection()《等价于 “and”运算》;

可以通过传入 ClassFilter、MethodMatcher、Pointcut 调用 union() 和 intersection();

使用案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import com.learn.aop.pointcut.controlflow.SimpleBeforeAdvice;
import com.learn.aop.pointcut.name.GrammyGuitarist;
import com.learn.aop.pointcut.name.Guitar;
import org.springframework.aop.Advisor;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.StaticMethodMatcher;
import java.lang.reflect.Method;

public class ComposablePointcutExample {
private static class SingMethodMatcher extends StaticMethodMatcher {
public boolean matches(Method method, Class<?> targetClass) {
return method.getName().startsWith("si");
}
}
private static class TalkMethodMatcher extends StaticMethodMatcher {
public boolean matches(Method method, Class<?> targetClass) {
return "talk".equals(method.getName());
}
}
private static class RestMethodMatcher extends StaticMethodMatcher {
public boolean matches(Method method, Class<?> targetClass) {
return method.getName().endsWith("st");
}
}
private static GrammyGuitarist getProxy(ComposablePointcut pointcut, GrammyGuitarist target) {
Advisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleBeforeAdvice());
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisor(advisor);
return ((GrammyGuitarist) proxyFactory.getProxy());
}
private static void testInvoke(GrammyGuitarist proxy) {
proxy.sing();
proxy.sing(new Guitar());
proxy.talk();
proxy.rest();
}
public static void main(String[] args) {
GrammyGuitarist target = new GrammyGuitarist();
ComposablePointcut pointcut = new ComposablePointcut(ClassFilter.TRUE, new SingMethodMatcher());
// 匹配si开头的方法
System.out.println("Test 1 >>> ");
GrammyGuitarist proxy = getProxy(pointcut, target);
testInvoke(proxy);
System.out.println();
// 匹配方法名为talk的方法
System.out.println("Test 2 >>> ");
pointcut.union(new TalkMethodMatcher());
proxy = getProxy(pointcut, target);
testInvoke(proxy);
System.out.println();
// 匹配st结尾的方法
System.out.println("Test 3 >>> ");
pointcut.intersection(new RestMethodMatcher());
proxy = getProxy(pointcut, target);
testInvoke(proxy);
System.out.println();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Test 1 >>> 
Before method: public void com.learn.aop.pointcut.name.GrammyGuitarist.sing()
sing()
Before method: public void com.learn.aop.pointcut.name.GrammyGuitarist.sing(com.learn.aop.pointcut.name.Guitar)
play(): G C GC AM D7
talk()
zzz~~

Test 2 >>>
Before method: public void com.learn.aop.pointcut.name.GrammyGuitarist.sing()
sing()
Before method: public void com.learn.aop.pointcut.name.GrammyGuitarist.sing(com.learn.aop.pointcut.name.Guitar)
play(): G C GC AM D7
Before method: public void com.learn.aop.pointcut.name.GrammyGuitarist.talk()
talk()
zzz~~

Test 3 >>>
sing()
play(): G C GC AM D7
talk()
zzz~~
  • “Test 1” 匹配si开头的方法,所以两个 sing() 方法都被通知;

  • “Test 2”匹配si开头的方法 或 匹配方法名为talk的方法,所以两个 sing() 方法 和 talk() 都被通知;

  • “Test 3” 匹配( 以si 开头 || talk()) && 以st 结尾, 没有方法符合,所以没有方法被通知;如果有一个方法名为 si***st() 那么,应该会被调用;

组合和切入点接口

org.springframework.aop.support.Pointcuts 接口也能用来 组合两个Pointcut,但更多Pointcut的组合还是试用ComposablePointcut;



引入入门

引入基础知识

一种特殊类型的环绕通知;

适用于类级别,不能在使用引入时 使用切入点,两者语义不匹配;

实现 接口 IntroductionInterceptor来创建引入:

继承 类 DelegatingIntroductionInterceptor 来创建引入:

引入通知 构成了 被通知对象 状态的一部分;因此,每个被通知对象 都必须有一个独立的引入实例;

使用引入进行对象修改检测

定义接口:

1
2
3
public interface IsModified {
boolean isModified();
}

创建通知Advice类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
// 创建通知
public class IsModifiedMixin extends DelegatingIntroductionInterceptor
implements IsModified {
private boolean isModified = false;
private Map<Method, Method> methodCache = new HashMap<Method, Method>();

public boolean isModified() {
return isModified;
}

private Method getGetter(Method setter) {
Method getter = methodCache.get(setter);
if (getter != null) {
return getter;
}
String getterName = setter.getName().replace("set", "get");
try {
getter = setter.getDeclaringClass().getMethod(getterName, null);
synchronized (methodCache) {
methodCache.put(setter, getter);
}
return getter;
} catch (NoSuchMethodException e) {
e.printStackTrace();
return null;
}
}

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
if (!isModified) {
if (mi.getMethod().getName().startsWith("set")
&& mi.getArguments().length == 1) {
Method getter = getGetter(mi.getMethod());
if (getter != null) {
Object newVal = mi.getArguments()[0];
Object oldVal = getter.invoke(mi.getThis(), null);
System.out.printf("old:%s, new:%s, %s \n", String.valueOf(oldVal), String.valueOf(newVal), mi.getThis().toString());
if (newVal == null && oldVal == null) {
isModified = false;
} else if (newVal == null && oldVal != null) {
isModified = true;
} else if (newVal != null && oldVal == null) {
isModified = true;
} else {
isModified = !newVal.equals(oldVal);
}
}
}
}
return super.invoke(mi);
}
}

创建顾问Advisor, 关联通知IsModifiedMixin:

1
2
3
4
5
6
7
import org.springframework.aop.support.DefaultIntroductionAdvisor;
// 创建顾问
public class IsModifiedAdvisor extends DefaultIntroductionAdvisor {
public IsModifiedAdvisor() {
super(new IsModifiedMixin());
}
}

创建目标对象Target:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 创建对象target
public class Contact {
private String name;
private String phoneNumber;
private String email;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}

组装 Advisor 通知 和 target:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import org.springframework.aop.IntroductionAdvisor;
import org.springframework.aop.framework.ProxyFactory;

// 组装 Advisor 通知 和 target
public class IntroductionDemo {
public static void main(String[] args) {
Contact target = new Contact();
target.setName("John Mayer");

IntroductionAdvisor advisor = new IsModifiedAdvisor();

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisor(advisor);
proxyFactory.setOptimize(true); // 使用CGLIB代理接口

Contact proxy = ((Contact) proxyFactory.getProxy());
IsModified proxyInterface = ((IsModified) proxy);
System.out.println("Is Contact? : " + (proxy instanceof Contact));
System.out.println("Is IsModified? : " + (proxy instanceof IsModified));
System.out.println("\nHas Modified? : " + proxyInterface.isModified());
proxy.setName("John Mayer");
System.out.println("\nHas Modified? : " + proxyInterface.isModified());
proxy.setName("Eric Clapton");
System.out.println("\nHas Modified? : " + proxyInterface.isModified());
proxy.setName("Eric Clapton");
System.out.println("\nHas Modified? : " + proxyInterface.isModified());
proxy.setName("John Mayer");
System.out.println("\nHas Modified? : " + proxyInterface.isModified());
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
Is Contact? : true
Is IsModified? : true
Has Modified? : false

old:John Mayer, new:John Mayer, com.learn.aop.inroduction.Contact@7d4793a8
Has Modified? : false

old:John Mayer, new:Eric Clapton, com.learn.aop.inroduction.Contact@7d4793a8
Has Modified? : true

Has Modified? : true

Has Modified? : true

proxy对象 既是 target, 又是IsModified;

proxyInterface.isModified() 第一次调用 查看初始状态John Mayer:未修改;

proxyInterface.isModified() 第二次调用 设为同样的值John Mayer:未修改;

proxyInterface.isModified() 第三次调用 设为新的值Eric Clapton:已经修改;

proxyInterface.isModified() 第四次调用 设为新的值Eric Clapton:已经修改;

proxyInterface.isModified() 第吴次调用 设为新的值Eric Clapton:已经修改;

目前只能检测一次变更;如果去掉 IsModifiedMixin 中的 if (!isModified) 就能检测多次变更;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Is Contact? : true
Is IsModified? : true
Has Modified? : false

old:John Mayer, new:John Mayer, com.learn.aop.inroduction.Contact@7d4793a8
Has Modified? : false

old:John Mayer, new:Eric Clapton, com.learn.aop.inroduction.Contact@7d4793a8
Has Modified? : true

old:Eric Clapton, new:Eric Clapton, com.learn.aop.inroduction.Contact@7d4793a8
Has Modified? : false

old:Eric Clapton, new:John Mayer, com.learn.aop.inroduction.Contact@7d4793a8
Has Modified? : true

引入小结

引入时Spring AOP最强大的 功能之一:不仅可以扩展现有方法的功能; 还可以动态扩展接口和对象实现。



AOP 框架事务

我们不得不编写大量的代码 来生成代理;

Spring提供了额外的框架,将代理注入到目标bean中;

以声明的方式配置AOP

使用Spring AOP 声明式配置时,存在三个选项:

  • 使用ProxyFactoryBean, ProxyFactoryBean提供了一种声明方式配置Spring的ApplicationContext(以及底层的BeanFactory);
  • 使用Spring AOP 命名空间:
  • 使用@AspectJ 样式注解:

使用ProxyFactoryBean

ProxyFactoryBean 允许 指定一个bean作为目标,并且为 该 bean 提供一组通知Advice 和 顾问Advisor(这些Advice 和 Advisor最终被合并到一个AOP代理中);

ProxyFactoryBean 将 拦截器 应用于现有的 目标bean;

使用AOP名称空间



使用 @AspectJ 注解



AspectJ集成



Spring 事务

发表于 2016-12-01 | 分类于 java , spring

linux epoll 水平触发LT和边缘出发ET

发表于 2016-11-30 | 分类于 linux

参考:

epoll、accept触发模式及阻塞方式的选择

ET or LT

select(),poll()模型都是水平触发模式,信号驱动IO是边缘触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发
从表象看epoll的性能最好,但是在连接数少,并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多回调函数来完成。

epoll工作在两种触发模式下:
Level_triggered(水平触发): 这是epoll默认的触发方式,既支持阻塞模式,也支持非阻塞模式,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上次没读写完的文件描述符上继续读写

Edge_triggered(边缘触发): 这种模式下,epoll只支持非阻塞模式,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。Nginx默认采用ET模式来使用epoll。

二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只要监测描述符上有数据,epoll_wait就会返回该socket。
所以,在epoll的ET模式下,正确的读写方式为:

读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN

这里的错误码是指:
在一个非阻塞的socket上调用read/write函数, 返回的errno为EAGAIN或者EWOULDBLOCK
这个错误表示资源暂时不够,read时,读缓冲区没有数据,或者write时,写缓冲区满了。遇到这种情况,如果是阻塞socket,read/write就要阻塞掉。而如果是非阻塞socket,read/write立即返回-1, 同时errno设置为EAGAIN。
简单代码示例如下:
读:

1
2
3
4
5
6
7
n = 0;
while ((nread = read(fd, buf + n, BUFSIZ - 1)) > 0) {
n += nread;
}
if (nread == -1 && errno != EAGAIN) {
perror("read error");
}

写:

1
2
3
4
5
6
7
8
9
10
11
12
13
int nwrite,
data_size = strlen(buf);
n = data_size;
while (n > 0) {
nwrite = write(fd, buf + data_size - n, n);
if (nwrite < n) {
if (nwrite == -1 && errno != EAGAIN) {
perror("write error");
}
break;
}
n -= nwrite;
}

从以上我们就看出来了为何 epoll在ET模式下使用的是非阻塞fd ,由于边缘触发的模式,每次epoll_wait返回就绪的fd,必须读完读取缓冲区里的所有数据(直至接收数据返回EAGAIN),必须套上while循环,此时若使用阻塞的fd,当读取完缓冲区里的数据后,接受数据过程会阻塞,从而无法接受新的连接。

1
**//////////////////////割割割割///////////////////////////////割割割割////////////////////////**

上面介绍完了epoll的两种触发模式以及两种阻塞方式的使用,下面分析一下网络编程中accept函数使用fd时要如何选择触发方式以及阻塞方式。

listenfd阻塞还是非阻塞?

如果TCP连接被客户端夭折,即在服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept调用上,就绪队列中的其他描述符都得不到处理。
解决办法是把监听套接口listenfd设置为非阻塞,当客户在服务器调用accept之前中止某个连接时,accept调用可以立即返回-1,这时源自Berkeley的实现会在内核中处理该事件,并不会将该事件通知给epool,而其他实现把errno设置为ECONNABORTED或者EPROTO错误,我们应该忽略这两个错误。
ET还是LT?
ET:如果多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。
解决办法是用while循环包住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。

LT:在nigix的实现中,accept函数调用使用水平触发的fd,就是出于对丢失连接的考虑(边缘触发时,accept只会执行一次接收一个连接,内核不会再去通知有连接就绪),所以使用水平触发的fd就不存在丢失连接的问题。但是如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。
所以服务器应该使用非阻塞的accept,并且在使用ET模式代码如下:

1
2
3
4
5
6
while ((conn_sock = accept(listenfd, (struct sockaddr * ) & remote, (size_t * ) & addrlen)) > 0) {
handle_client(conn_sock);
}
if (conn_sock == -1) {
if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR) perror("accept");
}

一道腾讯后台开发的面试题:
使用Linuxepoll模型,水平触发模式;当socket可写时,会不停的触发socket可写的事件,如何处理?
解答:正如我们上面说的,LT模式下不需要读写的文件描述符仍会不停地返回就绪,这样就会影响我们监测需要关心的文件描述符的效率。
所以这题的解决方法就是:平时不要把该描述符放进eventpoll结构体中,当需要写该fd的时候,调用epoll_ctl把fd加入eventpoll里监听,可写的时候就往里写,写完再次调用epoll_ctl把fd移出eventpoll,这种方法在发送很少数据的时候仍要执行两次epoll_ctl操作,有一定的操作代价
改进一下就是:平时不要把该描述符放进eventpoll结构体中,需要写的时候调用write或者send写数据,如果返回值是EAGAIN(写缓冲区满了),那么这时候才执行第一种方法的步骤。
归纳如下:
1.对于监听的sockfd要设置成非阻塞类型,触发模式最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,网上有的方案是用while来循环accept()。
2.对于读写的connfd,水平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置非阻塞。
3.对于读写的connfd,边缘触发模式下,必须使用非阻塞IO,并要一次性全部读写完数据。、
下面看一个在边沿触发模式下使用epoll的http服务器代码,必要的讲解都在注释里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/epoll.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <errno.h>

#define MAX_EVENTS 10
#define PORT 8080

//设置socket连接为非阻塞模式
void setnonblocking(int sockfd)
{
int opts;

opts = fcntl(sockfd, F_GETFL);
if(opts < 0)
{
perror("fcntl(F_GETFL)\n");
exit(1);
}
opts = (opts | O_NONBLOCK);
if(fcntl(sockfd, F_SETFL, opts) < 0)
{
perror("fcntl(F_SETFL)\n");
exit(1);
}
}

int main()
{
struct epoll_event ev, events[MAX_EVENTS];
int addrlen, listenfd, conn_sock, nfds, epfd, fd, i, nread, n;
struct sockaddr_in local, remote;
char buf[BUFSIZ];

//创建listen socket
{
perror("sockfd\n");
exit(1);
}
setnonblocking(listenfd);
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);;
local.sin_port = htons(PORT);
if( bind(listenfd, (struct sockaddr *) &local, sizeof(local)) < 0)
{
perror("bind\n");
exit(1);
}
listen(listenfd, 20);//设置为监听套接字

epfd = epoll_create(MAX_EVENTS);
{
perror("epoll_create");
exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listenfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1)
{
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}

for (;;)
{
nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);//超时时间-1,永久阻塞直到有事件发生
if (nfds == -1)
{
perror("epoll_pwait");
exit(EXIT_FAILURE);
}

for (i = 0; i < nfds; ++i)
{
fd = events[i].data.fd;

if (fd == listenfd) //如果是监听的listenfd,那就是连接来了,保存来的所有连接
{
//每次处理一个连接,while循环直到处理完所有的连接
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,
(size_t *)&addrlen)) > 0)
{
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;//边沿触发非阻塞模式
ev.data.fd = conn_sock;
//把连接socket加入监听结构体
if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: add");
exit(EXIT_FAILURE);
}
}
//已经处理完所有的连:accept返回-1,errno为EAGAIN
//出错:返回-1,errno另有其值
if (conn_sock == -1)
{
if (errno != EAGAIN && errno != ECONNABORTED
&& errno != EPROTO && errno != EINTR)
perror("accept");
}
continue;//直接开始下一次循环,也就是不执行这次循环后面的部分了
}
if (events[i].events & EPOLLIN) //可读事件
{
n = 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0)
{
n += nread;
}
if (nread == -1 && errno != EAGAIN)
{
perror("read error");
}
ev.data.fd = fd;
ev.events = events[i].events | EPOLLOUT;
//修改该fd监听事件类型,监测是否可写
if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1)
{
perror("epoll_ctl: mod");
}
}
if (events[i].events & EPOLLOUT) //可写事件
{
sprintf(buf, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\nHello World", 11);
int nwrite, data_size = strlen(buf);
n = data_size;
while (n > 0)
{
nwrite = write(fd, buf + data_size - n, n);
if (nwrite < n)
{
if (nwrite == -1 && errno != EAGAIN)
{
perror("write error");
}
break;
}
n -= nwrite;
}
//写完就关闭该连接socket
close(fd);
}
}
}

return 0;
}

epoll LT/ET 深入剖析

EPOLL事件有两种模型:

Level Triggered (LT) 水平触发
.socket接收缓冲区不为空 有数据可读 读事件一直触发
.socket发送缓冲区不满 可以继续写入数据 写事件一直触发
符合思维习惯,epoll_wait返回的事件就是socket的状态

Edge Triggered (ET) 边沿触发
.socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
.socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
仅在状态变化时触发事件

ET还是LT?

LT的处理过程:
. accept一个连接,添加到epoll中监听EPOLLIN事件
. 当EPOLLIN事件到达时,read fd中的数据并处理
. 当需要写出数据时,把数据write到fd中;如果数据较大,无法一次性写出,那么在epoll中监听EPOLLOUT事件
. 当EPOLLOUT事件到达时,继续把数据write到fd中;如果数据写出完毕,那么在epoll中关闭EPOLLOUT事件

ET的处理过程:
. accept一个一个连接,添加到epoll中监听EPOLLIN|EPOLLOUT事件
. 当EPOLLIN事件到达时,read fd中的数据并处理,read需要一直读,直到返回EAGAIN为止
. 当需要写出数据时,把数据write到fd中,直到数据全部写完,或者write返回EAGAIN
. 当EPOLLOUT事件到达时,继续把数据write到fd中,直到数据全部写完,或者write返回EAGAIN

从ET的处理过程中可以看到,ET的要求是需要一直读写,直到返回EAGAIN,否则就会遗漏事件。而LT的处理过程中,直到返回EAGAIN不是硬性要求,但通常的处理过程都会读写直到返回EAGAIN,但LT比ET多了一个开关EPOLLOUT事件的步骤

LT的编程与poll/select接近,符合一直以来的习惯,不易出错
ET的编程可以做到更加简洁,某些场景下更加高效,但另一方面容易遗漏事件,容易产生bug

这里有两个简单的例子演示了LT与ET的用法(其中epoll-et的代码比epoll要少10行):
https://github.com/yedf/handy/blob/master/raw-examples/epoll.cc
https://github.com/yedf/handy/blob/master/raw-examples/epoll-et.cc

针对容易触发LT开关EPOLLOUT事件的情景(让服务器返回1M大小的数据),我用ab做了性能测试
测试的结果显示ET的性能稍好,详情如下:
LT 启动命令 ./epoll a
ET 启动命令 ./epoll-et a
ab 命令:ab -n 1000 -k 127.0.0.1/
LT 结果:Requests per second: 42.56 [#/sec] (mean)
ET 结果:Requests per second: 48.55 [#/sec] (mean)

当我把服务器返回的数据大小改为48576时,开关EPOLLOUT更加频繁,性能的差异更大
ab 命令:ab -n 5000 -k 127.0.0.1/
LT 结果:Requests per second: 745.30 [#/sec] (mean)
ET 结果:Requests per second: 927.56 [#/sec] (mean)

对于nginx这种高性能服务器,ET模式是很好的,而其他的通用网络库,更多是使用LT,避免使用的过程中出现bug

epoll的LT和ET的区别

LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。

ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。

下面举一个列子来说明LT和ET的区别(都是非阻塞模式,阻塞就不说了,效率太低):
采用LT模式下, 如果accept调用有返回就可以马上建立当前这个连接了,再epoll_wait等待下次通知,和select一样。

但是对于ET而言,如果accpet调用有返回,除了建立当前这个连接外,不能马上就epoll_wait还需要继续循环accpet,直到返回-1,且errno==EAGAIN

epoll模型accept并发问题

服务端使用的是epoll网络模型。在测试的时候发现,单用户的情况下客户端和服务器通信正常。但是在多用户并发的情况下,客户端和服务端通信不正常。此时,客户端能正常的链接,发送数据,但是一直卡在接收数据部分

出现这种问题,是因为不正确的使用了epoll中的ET(edge-trigger)模式。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**************************************************
函数名:acceptConn
功能:接受客户端的链接
参数:srvfd:监听SOCKET
***************************************************/
void acceptConn(int srvfd) {
struct sockaddr_in sin;
socklen_t len = sizeof(struct sockaddr_in);
bzero( & sin, len);

int confd = accept(srvfd, (struct sockaddr * ) & sin, &len);

if (confd < 0) {
printf("%s: bad accept\n");
return;
} else {
printf("Accept Connection: %d", confd);
}

setNonblocking(confd);

//将新建立的连接添加到EPOLL的监听中
struct epoll_event event;
event.data.fd = confd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &event);
}

注意倒数第二行:event.events = EPOLLIN|EPOLLET; 采用的是ET模式。下面我们来具体说下,问题出在那里。

在epoll中有两种模式:level-trigger模式,简称LT模式,和edge-trigger模式,简称ET模式。其中,LT是默认的工作模式。

这两种模式的工作方式有些不同。在level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。

在ET模式socket非阻塞的情况下(上面代码中就是这种情况),多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。因此,就出现了上面所提及的问题。

解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。

修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**************************************************
函数名:acceptConn
功能:接受客户端的链接
参数:srvfd:监听SOCKET
***************************************************/
void acceptConn(int srvfd) {
struct sockaddr_in sin;
socklen_t len = sizeof(struct sockaddr_in);
bzero( & sin, len);
int confd = 0;
while ((confd = accept(srvfd, (struct sockaddr * ) & sin, &len)) > 0) {
printf("Accept Connection: %d", confd);

setNonblocking(confd);

//将新建立的连接添加到EPOLL的监听中
struct epoll_event event;
event.data.fd = confd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &event);
}
if (confd == -1) {
if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR) {
printf("%s: bad accept\n");
return;
}
}
}

同理,接收数据和发送数据时如果是ET模式,且非阻塞,也得用循环。

读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN

写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN

正确的读:

1
2
3
4
5
6
7
n = 0;
while ((nread = read(fd, buf + n, BUFSIZ - 1)) > 0) {
n += nread;
}
if (nread == -1 && errno != EAGAIN) {
perror("read error");
}

正确的写:

1
2
3
4
5
6
7
8
9
10
11
12
13
int nwrite,
data_size = strlen(buf);
n = data_size;
while (n > 0) {
nwrite = write(fd, buf + data_size - n, n);
if (nwrite < n) {
if (nwrite == -1 && errno != EAGAIN) {
perror("write error");
}
break;
}
n -= nwrite;
}

epoll中accept的使用细节

accept 要考虑 2 个问题
(1) 阻塞模式 accept 存在的问题
考虑这种情况:TCP连接被客户端夭折,即在服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,
直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept调用上,就绪队列中的其他描述符都得不到处理。

解决办法: 把监听套接口设置为非阻塞,当客户在服务器调用accept之前中止某个连接时,accept调用可以立即返回-1,这时源自Berkeley的实现会在内核中处理该事件,并不会将该事件通知给epoll,而其他实现把errno设置为ECONNABORTED或者EPROTO错误,我们应该忽略这两个错误。

(2)ET模式下accept存在的问题
考虑这种情况:多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。

解决办法: 用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。综合以上两种情况,服务器应该使用非阻塞地accept,accept在ET模式下的正确使用方式为:

1
2
3
4
5
6
7
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) {
handle_client(conn_sock);
}
if (conn_sock == -1) {
if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)
perror("accept");
}

netty为什么选择使用LT

epoll对文件描述符有两种操作模式–LT(level trigger水平模式)和ET(edge trigger边缘模式)

简单来讲,LT是epoll的默认操作模式,当epoll_wait函数检测到有事件发生并将通知应用程序,而应用程序不一定必须立即进行处理,这样epoll_wait函数再次检测到此事件的时候还会通知应用程序,直到事件被处理。

而ET模式,只要epoll_wait函数检测到事件发生,通知应用程序立即进行处理,后续的epoll_wait函数将不再检测此事件。因此ET模式在很大程度上降低了同一个事件被epoll触发的次数,因此效率比LT模式高。

解释为什么epoll默认是LT的原因(超哥解释,个人觉得还是非常不错的)LT(level triggered):LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。

io.netty:netty-transport-native-epoll:linux-x86_64:4.1.49.Final

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
public enum EpollMode {
// Use {@code EPOLLET} (edge-triggered).
EDGE_TRIGGERED,
// Do not use {@code EPOLLET} (level-triggered).
LEVEL_TRIGGERED
}
final class NativeStaticallyReferencedJniMethods {
static native int epollin();
static native int epollout();
static native int epollrdhup();
static native int epollet();
static native int epollerr();
static native long ssizeMax();
static native int tcpMd5SigMaxKeyLen();
static native int iovMax();
static native int uioMaxIov();
static native boolean isSupportingSendmmsg();
static native boolean isSupportingRecvmmsg();
static native boolean isSupportingTcpFastopen();
static native String kernelVersion();
}
public final class Native {
static {
public static final int EPOLLET = epollet();
}
}
public class EpollChannelConfig extends DefaultChannelConfig {
public EpollMode getEpollMode() {
return ((AbstractEpollChannel) channel).isFlagSet(Native.EPOLLET)
? EpollMode.EDGE_TRIGGERED : EpollMode.LEVEL_TRIGGERED;
}

public EpollChannelConfig setEpollMode(EpollMode mode) {
ObjectUtil.checkNotNull(mode, "mode");

try {
switch (mode) {
case EDGE_TRIGGERED:
checkChannelNotRegistered();
((AbstractEpollChannel) channel).setFlag(Native.EPOLLET);
break;
case LEVEL_TRIGGERED:
checkChannelNotRegistered();
((AbstractEpollChannel) channel).clearFlag(Native.EPOLLET);
break;
default:
throw new Error();
}
} catch (IOException e) {
throw new ChannelException(e);
}
return this;
}
}

class EpollEventLoop extends SingleThreadEventLoop {
EpollEventLoop(EventLoopGroup parent, Executor executor, int maxEvents,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
EventLoopTaskQueueFactory queueFactory) {
super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory),
rejectedExecutionHandler);
selectStrategy = ObjectUtil.checkNotNull(strategy, "strategy");
if (maxEvents == 0) {
allowGrowing = true;
events = new EpollEventArray(4096);
} else {
allowGrowing = false;
events = new EpollEventArray(maxEvents);
}
boolean success = false;
FileDescriptor epollFd = null;
FileDescriptor eventFd = null;
FileDescriptor timerFd = null;
try {
this.epollFd = epollFd = Native.newEpollCreate();
this.eventFd = eventFd = Native.newEventFd();
try {
// It is important to use EPOLLET here as we only want to get the notification once per
// wakeup and don't call eventfd_read(...).
Native.epollCtlAdd(epollFd.intValue(), eventFd.intValue(), Native.EPOLLIN | Native.EPOLLET);
} catch (IOException e) {
throw new IllegalStateException("Unable to add eventFd filedescriptor to epoll", e);
}
this.timerFd = timerFd = Native.newTimerFd();
try {
// It is important to use EPOLLET here as we only want to get the notification once per
// wakeup and don't call read(...).
Native.epollCtlAdd(epollFd.intValue(), timerFd.intValue(), Native.EPOLLIN | Native.EPOLLET);
} catch (IOException e) {
throw new IllegalStateException("Unable to add timerFd filedescriptor to epoll", e);
}
success = true;
} finally {
if (!success) {
if (epollFd != null) {
try {
epollFd.close();
} catch (Exception e) {
// ignore
}
}
if (eventFd != null) {
try {
eventFd.close();
} catch (Exception e) {
// ignore
}
}
if (timerFd != null) {
try {
timerFd.close();
} catch (Exception e) {
// ignore
}
}
}
}
}
}

linux网络流量监控

发表于 2016-11-20 | 分类于 linux

iftop

centos7:

1
2
3
4
yum install -y iftop
ifconfig -a

iftop -i eth0 -n -P


watch cat /proc/net/dev

监控网卡流量

1
watch cat /proc/net/dev

iftop编译安装

1
2
3
4
5
6
7
8
yum install -y flex byacc libpcap ncurses-devel libpcap-devel   #先要安装必需的软件  
mkdir iftop
cd iftop/
wget http://www.ex-parrot.com/pdw/iftop/download/iftop-1.0pre4.tar.gz #下载
tar zxvf iftop-1.0pre4.tar.gz #解压
cd iftop-1.0pre4
./configure #配置
make && make install #编译安装

命令说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
语法: iftop -h | [-npblNBP] [-i interface] [-f filter code] [-F net/mask] [-G net6/mask6]  
-h 显示本帮助(Help)信息
-n 不进行主机名(hostName)查找
-N 不将端口号(port Number)转换成对应的服务 to services
-p 混合(Promiscuous)模式(显示网络相关的其他主机信息)
-b 不显示流量图形条(Bar)
-B 以字节(Byte)为单位,显示带宽(Bandwidth);默认以比特(bit)显示的
-i interface 监控的网卡接口(interface)
-f filter code 包统计时,使用过滤码;默认:无,只统计IP包
-F net/mask 显示特定IPv4网段的进出流量(Flow);如# iftop -F 10.10.1.0/24
-G net6/mask6 显示特定IPv6网段的进出流量(Flow)
-l 显示并统计IPv6本地(Local)链接的流量(默认:关)
-P 显示端口(Port)
-m limit 设置显示界面上侧的带宽刻度(liMit)
-c config file 指定配置(Config)文件
-t 使用不带窗口菜单的文本(text)接口

排序:
-o 2s Sort by first column (2s traffic average)
-o 10s Sort by second column (10s traffic average) [default]
-o 40s Sort by third column (40s traffic average)
-o source Sort by source address
-o destination Sort by destination address

The following options are only available in combination with -t
-s num print one single text output afer num seconds, then quit
-L num number of lines to print

centos7 tcp缓冲区和udp缓冲区大小设置_未完

发表于 2016-11-17 | 分类于 linux


设置TCP缓冲区大小


设置UPD缓冲区大小

鉴于tcp有重传机制,更多的时候udp对收发缓冲区的大小可能更加敏感一点。

udp缓冲区的大小主要和以下几个值有关:

  1. /proc/sys/net/core/rmem_max ——— udp缓冲区的最大值,单位字节,下同
  2. /proc/sys/net/core/rmem_default ———- udp缓冲区的默认值,如果不更改的话程序的udp缓冲区默认值就是这个。

查看方法可以直接 cat 以上两个文件进行查看,也可以通过 sysctl 查看。

sysctl -a | grep rmem_max

其实sysctl信息来源就是 proc 下的文件。

程序中进行更改

程序中可以使用setsockopt函数与SO_RCVBUF选项对udp缓冲区的值进行更改,但是要注意不管设置的值有多大,超过rmem_max的部分都会被无视。

int a = value_wanted;

if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &a, sizeof(int)) == - 1 ) {

…

}

更改系统值

如果确实要把udp缓冲区改到一个比较大的值,那就需要更改rmem_max的值。编辑/etc/rc.local文件添加以下代码可使系统在每次启动的时候自动更改系统缓冲区的最大值。

echo value_wanted > / proc /sys/net/core/rmem_default

或者在/etc/sysctl.conf添加以下代码即可在重启后永久生效。

rmem_max = MAX

不想重启的话使用命令 sysctl -p 即可。

可以顺便看下setsockopt在linux下的相关实现

1
2
3
4
5
6
7
case SO_SNDBUF:
if (val > sysctl_wmem_max)
val = sysctl_wmem_max;
if ((val * 2 ) < SOCK_MIN_SNDBUF)
sk->sk_sndbuf = SOCK_MIN_SNDBUF;
else
sk->sk_sndbuf = val * 2 ; //当然缓冲区在系统中的实际值要大一点,因为udp报头以及IP报头等都是需要空间的。

Linux性能分析工具总纲_未完

发表于 2016-11-11 | 分类于 linux


linux 更详细的性能分析工具网址:http://www.brendangregg.com/

CSDN中文介绍:https://www.cnblogs.com/tcicy/p/8461807.html

img

DNS解析过程

发表于 2016-09-02 | 分类于 computer-network


先说一下DNS的几个基本概念:

一. 根域

就是所谓的“.”,其实我们的网址www.baidu.com在配置当中应该是www.baidu.com.(最后有一点),一般我们在浏览器里输入时会省略后面的点,而这也已经成为了习惯。

根域服务器我们知道有13台,但是这是错误的观点。

根域服务器只是具有13个IP地址,但机器数量却不是13台,因为这些IP地址借助了任播的技术,所以我们可以在全球设立这些IP的镜像站点,你访问到的这个IP并不是唯一的那台主机。

具体的镜像分布可以参考维基百科。这些主机的内容都是一样的

二. 域的划分

根域下来就是顶级域或者叫一级域,

有两种划分方式,一种互联网刚兴起时的按照行业性质划分的com.,net.等,一种是按国家划分的如cn.,jp.,等。

具体多少你可以自己去查,我们这里不关心。

每个域都会有域名服务器,也叫权威域名服务器。

Baidu.com就是一个顶级域名,而www.baidu.com却不是顶级域名,他是在baidu.com 这个域里的一叫做www的主机。

一级域之后还有二级域,三级域,只要我买了一个顶级域,并且我搭建了自己BIND服务器(或者其他软件搭建的)注册到互联网中,那么我就可以随意在前面多加几个域了(当然长度是有限制的)。

比如a.www.baidu.com,在这个网址中,www.baidu.com变成了一个二级域而不是一台主机,主机名是a。

三. 域名服务器

能提供域名解析的服务器,上面的记录类型可以是A(address)记录,NS记录(name server),MX(mail),CNAME等。

(详解参见博客:域名解析中A记录、CNAME、MX记录、NS记录的区别和联系)

A记录是什么意思呢,就是记录一个IP地址和一个主机名字,比如我这个域名服务器所在的域test.baidu.com,我们知道这是一个二级的域名,然后我在里面有一条A记录,记录了主机为a的IP,查到了就返回给你了。

如果我现在要想baidu.com这个域名服务器查询a.test.baidu.com,那么这个顶级域名服务器就会发现你请求的这个网址在test.baidu.com这个域中,我这里记录了这个二级域的域名服务器test.baidu.com的NS的IP。我返回给你这个地址你再去查主机为a的主机把。

这些域内的域名服务器都称为权威服务器,直接提供DNS查询服务。(这些服务器可不会做递归哦)

四.解析过程

那么我们的DNS是怎么解析一个域名的呢?

1、现在我有一台计算机,通过ISP接入了互联网,那么ISP就会给我分配一个DNS服务器,这个DNS服务器不是权威服务器,而是相当于一个代理的dns解析服务器,他会帮你迭代权威服务器返回的应答,然后把最终查到IP返回给你。

2、现在的我计算机要向这台ISPDNS发起请求查询www.baidu.com这个域名了,(经网友提醒:这里其实准确来说不是ISPDNS,而应该是用户自己电脑网络设置里的DNS,并不一定是ISPDNS。比如也有可能你手工设置了8.8.8.8)

3、ISPDNS拿到请求后,先检查一下自己的缓存中有没有这个地址,有的话就直接返回。这个时候拿到的ip地址,会被标记为非权威服务器的应答。

4、如果缓存中没有的话,ISPDNS会从配置文件里面读取13个根域名服务器的地址(这些地址是不变的,直接在BIND的配置文件中),

5、然后像其中一台发起请求。

6、根服务器拿到这个请求后,知道他是com.这个顶级域名下的,所以就会返回com域中的NS记录,一般来说是13台主机名和IP。

7、然后ISPDNS向其中一台再次发起请求,com域的服务器发现你这请求是baidu.com这个域的,我一查发现了这个域的NS,那我就返回给你,你再去查。

(目前百度有4台baidu.com的顶级域名服务器)。

8、ISPDNS不厌其烦的再次向baidu.com这个域的权威服务器发起请求,baidu.com收到之后,查了下有www的这台主机,就把这个IP返回给你了,

9、然后ISPDNS拿到了之后,将其返回给了客户端,并且把这个保存在高速缓存中。

下面我们来用 nslookup 这个工具详细来说一下解析步骤:

1
yum install bind-utils -y

从上图我们可以看到:

​ 第一行Server是:DNS服务器的主机名–210.32.32.1

​ 第二行Address是: 它的IP地址–210.32.32.1#53

​ 下面的Name是:解析的URL– www.jsjzx.com

​ Address是:解析出来的IP–112.121.162.168

但是也有像百度这样的DNS比较复杂的解析:

你会发现百度有一个cname = www.a.shifen.com 的别名。

这是怎么一个过程呢?

我们用dig工具来跟踪一下吧(linux系统自带有)

-—————————————————————————————————————————————————————————————————————————–

Dig工具会在本地计算机做迭代,然后记录查询的过程。

第一步是向我这台机器的ISPDNS获取到根域服务区的13个IP和主机名[b-j].root-servers.net.。

第二步是向其中的一台根域服务器(Servername就是末行小括号里面的)发送www.baidu.com的查询请求,他返回了com.顶级域的服务器IP(未显示)和名称

第三步,便向com.域的一台服务器192.43.172.30请求,www.baidu.com,他返回了baidu.com域的服务器IP(未显示)和名称,百度有四台顶级域的服务器

​ 【此处可以用dig @192.43.172.30 www.baidu.com查看返回的百度顶级域名服务器IP地址】。

第四步呢,向百度的顶级域服务器(202.181.33.31)请求www.baidu.com,他发现这个www有个别名,而不是一台主机,别名是www.a.shifen.com。

-——————————————————————————————————————————————————————————————————————————–

按照一般的逻辑,当dns请求到别名的时候,查询会终止,然后重新发起查询别名的请求,所以此处应该返回的是www.a.shifen.com。

此处用dig +trace [www.a.shifen.com】跟踪一下

用一个图来说明一下

以下内容为在虚拟机中搭建local dns服务器得到的实验数据,纠正上述结论

在上面的分析中,我们用dig工具进行了追踪,但是dig没有继续追踪当我们从baidu.com拿到cname和ns2.a.shifen.com的IP之后的事情。

我们就所以然的下结论认为local dns会向ns2.a.shifen.com请求www.a.shifenc.om。

其实这个想法是错误,在自己的本地搭建一个local dns,抓取整个解析过程中是所有包,看看就明白拉。

实际的结果是虽然dns.baidu.com返回了a.shifen.com域的服务器地址和IP,

但是local dns并不是直接向上述返回的IP请求www.a.shifen.com,而是再一次去请求com域,得到shifen.com域的服务器(也就是baidu.com的那四台),

然后又请求www.a.shifen.com,返回a.shifen.com的域的服务器,最后才是去请求www.a.shifen.com,

虽然上面已经返回了IP,但是实验的结果就是再走一遍shifen.com域的查询。

上图就是localdns在解析www.baidu.com的抓包全过程。蓝色那条就是在收到cname和响应的a.shifen.com的域名服务器IP地址之后,继续向com域请求shifen.com。

这个图充分说明了返回cname的同时也返回了ns2.a.shifen.com的IP。

因此总结一下便是

​ ①本机向local dns请求www.baidu.com

​ ②local dns向根域请求www.baidu.com,根域返回com.域的服务器IP

​ ③向com.域请求www.baidu.com,com.域返回baidu.com域的服务器IP

​ ④向baidu.com请求www.baidu.com,返回cname www.a.shifen.com和a.shifen.com域的服务器IP

​ ⑤向root域请求www.a.shifen.com

​ ⑥向com.域请求www.a.shife.com

​ ⑦向shifen.com请求

​ ⑧向a.shifen.com域请求

​ ⑨拿到www.a.shifen.com的IP

​ ⑩localdns返回本机www.baidu.com cname www.a.shifen.com 以及 www.a.shifen.com的IP先说一下DNS的几个基本概念:

1、lambda表达式入门:Java多核编程

发表于 2016-08-11 | 分类于 java , lambda


第1章 新生代Java

1、外部迭代 -> 内部迭代

外部迭代示例

1
2
3
for (Point p : pointList) {
p.distance(0,0); // 点p 到 原点(0,0)的距离
}

内部迭代示例

1
pointList.forEach(p -> p.distance(0,0));

1.1、内部迭代

外部迭代 -> 串行、有序

内部迭代 -> 顺序不重要,重要的是行为;

​ -> 延迟加载,乱序执行;

1.2、命令模式

类似与命令模式,将 行为 抽象出来,作为Action;

1
2
3
4
5
6
7
8
9
10
11
12
// 行为 接口
public interface Consumer<T> {
void accept(T t);
}
// 具体定义行为
public Distance implements Consumer<Point> {
public void accept(Point p) {
p.distance(0, 0);
}
}
// 为了调用 p.distance(0, 0); 不断创建对象,而且不够直观
pointList.forEach(new Distance());

为了调用 p.distance(0, 0); 不断创建对象,而且不够直观。

这是因为Java 方法只接受 对象引用 作为参数,因此要引入新语法 lambda 表达式

1.3、lambda 表达式

1
2
3
4
5
pointList.forEach(new Consumer<Point> {
public void accept(Point p) {
p.distance(0, 0);
}
});

Consumer 是一种 ”单方法接口“,有利于 编译器直接 无歧义 的选择正确的方法执行;

引入一个额外的愈发(”->“)之后,将 参数与 表达式 分隔开,得到简单形式的 lambda表达式:

1
pointList.forEach(p -> p.distance(0,0));

2、集合 -> 流

场景:一个Integer集合 —> 转换成一个 Point实例 集合 —> 寻找 距离原点(0, 0) 最远的点到原点的距离;

集合的写法

1
2
3
4
5
6
7
8
9
List<Integer> intList = Arrays.asList(1,2,3,4,5);
List<Point> pointList = new ArrayList<>();
for (Integer i : intList) {
pointList.add(new Point(i%3, i/1));
}
Double maxDistance = Double.MIN_VALUE;
for (Point p : pointList) {
maxDistance = Math.max(maxDistance, p.distance(0, 0));
}

假如把 intList 看作一个无限流:

1
2
3
4
5
intStream = intList.stream();
Stream<Point> pointStream = intStream.map(i -> new Point(i%3, i/1));
DoubleStream distanceStream = pointStream.mapToDouble(p -> p.distance(0, 0));
// 管道最后是终止操作max
OptionalDouble maxDistance = distances.max();

完整的代码变成了如下:

1
2
3
4
5
OptionalDouble maxDistance = intList
.stream()
.map(i -> new Point(i%3, i/1))
.mapToDouble(p -> p.distance(0, 0))
.max();

使用管道的好处是, 创建与管理中间集合所导致的性能消耗也随之消失;


3、串行 -> 并行

Java 8 支持对集合的并行处理,而 lambda表达式 是”提供这种支持“ 必备的一环;

原理:

  1. 将大任务 递归的 切分为 ”足够小“ 的 小任务(能串行执行为止);
  2. 使用 cpu 的多核 分别执行完以后;
  3. 将结果 结合起来 返回;

实现方式:fork/join 框架

代码:

1
2
3
4
5
OptionalDouble maxDistance = intList
.parallelStream()
.map(i -> new Point(i%3, i/1))
.mapToDouble(p -> p.distance(0, 0))
.max();

什么是 “足够小”的任务? 取决于 可用核的数量;


4、组合行为

Java 原版的排序器

1
2
3
4
5
Comparator<Point> byX = new Comparator<Point>() {
public int compare(Point p1, Point p2) {
return Double.compare(p1.getX(), p2.getX());
}
}

创建一个通用的 排序器 Comparator

1
2
3
4
5
public static <T,U extends Comparable<U>> 
Compatable<T> comparing(Function<T, U> keyExtractor) {
return (c1, c2) ->
keyExtractor.apply(c1).compateTo(keyExtractor.apply(c2));
}

那么 1.3 中的问题,根据距离排序 可以实现为

1
Comparator<Point> byDistance = comparing(p -> p.distance(0, 0));

新需求:按所有点到 原点的距离 升序排列,打印点:

1
2
3
4
5
intList
.stream()
.map(i -> new Point(i%3, i/1))
.sorted(comparing(p -> p.distance(0, 0)))
.forEach(p -> System.out.printf("(%f, %f)", p.getX(), p.getY()));


K 个一组翻转链表

发表于 2016-04-09

算法分类: LinkedList

url:https://leetcode-cn.com/problems/reverse-nodes-in-k-group/

​

题目:K 个一组翻转链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

 

示例:

给你这个链表:1->2->3->4->5

当 k = 2 时,应当返回: 2->1->4->3->5

当 k = 3 时,应当返回: 3->2->1->4->5

 

说明:

你的算法只能使用常数的额外空间。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reverse-nodes-in-k-group
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

​

Java解法

1
2


​

Python解法

1
2


Go解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package main

import "fmt"

func main() {
a := &ListNode {
Val : 1,
}
b := &ListNode {
Val : 2,
}
c := &ListNode {
Val : 3,
}
d := &ListNode {
Val : 4,
}
e := &ListNode {
Val : 5,
}
a.Next = b
b.Next = c
c.Next = d
d.Next = e
e.Next = nil

head := reverseKGroup(a, 2)
//head := a
for head != nil {
fmt.Print(head.Val)
head = head.Next
}
}

/**
* K 个一组翻转链表
* https://leetcode-cn.com/problems/reverse-nodes-in-k-group/
*/

type ListNode struct {
Val int
Next *ListNode
}

func reverseKGroup(head *ListNode, k int) *ListNode {
cur := head
for i := 1; i < k && cur != nil; i++ {
cur = cur.Next
}

if cur == nil {
return head
}
next := cur.Next
cur.Next = nil
cur = reverseList(head)
head.Next = reverseKGroup(next, k)
return cur
}

func reverseList(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
var next *ListNode
h := head
pre := head
cur := head.Next
for cur != nil {
next = cur.Next
cur.Next = pre
pre = cur
cur = next
}
h.Next = nil
head = pre
return pre
}

image-20201102235821468

1…202122…25

Focus-1

250 日志
63 分类
102 标签
Links
  • repository - https://gitee.com/carloz
© 2015 — 2020 Focus-1
Hosted by Gitee Repo