成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

模仿 Spring 實現一個類管理容器

網絡
項目的初衷是獨立作出一個成熟的有特色的IOC容器,但由于過程參考Spring太多,而且也無法作出太多改進,于是目的變為以此項目作為理解Spring的一個跳板,與網上的一些模仿Spring的框架不同,本項目主要是針對注解形式 地址是Thales

概述

項目的初衷是獨立作出一個成熟的有特色的IOC容器,但由于過程參考Spring太多,而且也無法作出太多改進,于是目的變為以此項目作為理解Spring的一個跳板,與網上的一些模仿Spring的框架不同,本項目主要是針對注解形式
地址是Thales

流程

在Spring中,一個bean的形成分三個大的階段,

bean的定義階段(包含BeanDefinition的加載,解析,與注冊)
bean的實例化階段(包含對象的創建,屬性的注入)
bean的初始化階段(包含一些資源的初始化,譬如打開文件建立連接等等)
這只是大概的劃分,關于BeanPostProcessor等后置處理并沒有顯式的提及.

類的設計

如果只想了解一個bean是怎么從生到死的,只需要一步步debug就好了,如果看不懂,就多debug幾遍.可是如果想實現一個類似的容器,類的設計,職責的分配,接口的實現繼承必然是要了解的(除非你想幾個類做完所有的事)

以下是DefaultListableBeanFactory的類圖

是不是頂不住

我們再來看一張圖

第一張是Spring5.0的,第二張圖是Spring0.9的,所以并沒有必要在一開始就引入過多的設計復雜度

我們再來看一套對比圖

哪一個是0.9的,哪一個是5.0的一目了然.

說這么多的目的,是說明我們沒必要一開始就奔著最完善的目標去寫,可以一步步來,一步步加入功能

實現簡易IOC
眾所周知,SpringIoC中最基本的就是BeanFactory

我們先定義一個BeanFactory接口

  1. //暫時就給這一個方法public interface BeanFactory {    /**     * 根據名字獲取Bean實例     * @param name     * @return     */    Object getBean(String name);} 

beanDefinition

由于是注解形式,我們不能再像xml那樣給定一個資源文件再去解析了,而應該去掃描classPath下所有帶有@Component的類,

這時候我們需要給定的參數就從文件路徑變成了包路徑,我們只需要掃描這個包及其子包內符合條件的類,并且將其轉化為BeanDefinition再注冊就好.執行這個功能的是ClassPathBeanDefinitionScanner這個類.在這一步,就已經和傳統的流程有所區別了,我們會傳入一個ResourceLoader去實現具體的掃描功能(即定位),但不會再有專門的類去處理解析這一步

  1. public interface Resource {    File getFile();    String getFilename();    String getFilePath();}//在最初設計的時候這個抽象類似乎沒有用,但考慮到以后的擴展,還是先放在這public abstract class AbstractResource implements Resource {    @Override    public String getFilename() {        return getFile().getName();    }    @Override    public String getFilePath() {        return getFile().getPath();    }}//這就是最終我們實例化bean時用到的Resource類,在Spring中并沒有直接用,而是通過外觀模式集成了一下成為RootBeanDefinitionpublic class ClassPathResource extends AbstractResource {    private final String path;    private ClassLoader classLoader;    private Class<?> clazz;    public ClassPathResource(String path, ClassLoader classLoader, Class<?> clazz) {        this.path = path;        this.classLoader = classLoader;        this.clazz = clazz;    }} 

  1. public interface ResourceLoader {    Resource getResource(String location);}//此類能夠實現加載多個資源public interface ResourcePatternResolver extends ResourceLoader {    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";    List<? extends Resource> getResources(String location);}//這個類就是正式用于掃描的類了public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {    private final ResourceLoader resourceLoader;    public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader){        this.resourceLoader = resourceLoader;    }    @Override    public Resource getResource(String location) {        return resourceLoader.getResource(location);    }//在Spring中,是通過一層層方法的包裝完成包名到路徑的轉換再到每個文件的掃描再轉換為Resource,這里暫時就先一步到位,把具體實現放在工具類里    @Override    public List<? extends Resource> getResources(String location) {        Set<Class<?>> classes = ClassUtils.getClasses(location);        List<ClassPathResource> classPathResources = new ArrayList<>();        for (Class<?> clazz:classes) {            classPathResources.add(new ClassPathResource("",clazz.getClassLoader(),clazz));        }        return classPathResources;    }} 

但最后直接使用的并不是PathMatchingResourcePatternResolver

而是把他作為ClassPathBeanDefinitionScanner的一個屬性,在這個類里調用.

我們得到了Resource,如何獲得對應的BeanDefinition?

先考慮這樣一個問題,什么樣的類可以被注冊BeanDefinition?

添加了@Component注解或者滿足其他注冊的條件
不是接口或者抽象類
所以我們可以單獨抽象出一個方法 boolean isCandidateComponent(Class<?> clazz)來判斷是否被注冊

現在到了注冊階段,依舊秉持面向接口編程的理念,同時考慮到單一職責,我們把注冊Bean定義單獨抽象出來

  1. public interface BeanDefinitionRegistry {    void registerBeanDefinition(BeanDefinition beanDefinition);} 

上文說到Bean定義的定位,解析,注冊都是在ClassPathBeanDefinitionScanner里完成的,于是BeanDefinitionRegistry自然也成為了ClassPathBeanDefinitionScanner的屬性之一

于是ClassPathBeanDefinitionScanner構建完成了

  1. public class ClassPathBeanDefinitionScanner {    //負責具體的Resource定位    private ResourcePatternResolver resourcePatternResolver;    //負責BeanDefinition解析    private BeanDefinitionRegistry registry;    public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry,String...basePackage) {        this.registry = registry;        this.resourcePatternResolver = new PathMatchingResourcePatternResolver((ResourceLoader) registry);        this.scan(basePackage);    }    public void scan(String...basePackages){        doScan(basePackages);    }    void doScan(String[] basePackages){        Set<BeanDefinition> beanDefinitions = new LinkedHashSet<>();        for (String basePackage:basePackages) {            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);            for(BeanDefinition candidate:candidates){                beanDefinitions.add(candidate);                registry.registerBeanDefinition(candidate);            }        }    }    //獲取被注冊的bean的集合    private Set<BeanDefinition> findCandidateComponents(String basePackage) {        Set<BeanDefinition> candidates = new LinkedHashSet<>();        List<? extends Resource> resources = getResourcePatternResolver().getResources(basePackage);        for(Resource resource:resources){            if(resource instanceof ClassPathResource){                ClassPathResource classPathResource = (ClassPathResource)resource;                if(isCandidateComponent(classPathResource.getClazz())){                    AnnotationBeanDefinition beanDefinition = new AnnotationBeanDefinition();                    beanDefinition.setClazz(classPathResource.getClazz());                    beanDefinition.setBeanName(BeanUtils.generateBeanName(classPathResource.getClazz().getName()));                    candidates.add(beanDefinition);                }            }        }        return candidates;    }    private ResourcePatternResolver getResourcePatternResolver() {        return this.resourcePatternResolver;    }    //判斷是否被注冊    boolean isCandidateComponent(Class<?> clazz){        Component declaredAnnotation = clazz.getDeclaredAnnotation(Component.class);        return declaredAnnotation!=null&&!clazz.isInterface();    }    ;} 

實例化

在什么時候實例化?我們說,在調用getBean()而又沒有現成的bean時進行實例化

  1. public abstract class AbstractBeanFactory implements BeanFactory{    @Override    public Object getBean(String beanName)    } 

對象創建

有兩種方式,通過Jdk默認的反射實現,或者用cglib代理實現.

默認自然是無參構造,但是如果傳入了參數,則需要根據參數的類型和數量去匹配對應的構造函數,用其去實例化

于是我們抽象出InstantiationStrategy作為實例化接口,兩種實例化方法都需要實現這個接口,我們真正去用的時候只需要去調該接口的方法就好

  1. public interface InstantiationStrategy {    Object instantiate(BeanDefinition beanDefinition, String beanName, BeanFactory owner);}public class SimpleInstantiationStrategy implements InstantiationStrategy {} 

屬性注入

字段值獲取

有兩種方式可以實現字段值獲取

直接注解Autowired或者Value
Value里面填的不是值而是占位符,那么就需要解析占位符去獲取
我們通過Class對象獲取所有字段,再通過遍歷所有字段查找加在字段上的注解來獲取(這僅僅只是Spring的一種注入方式)

  1. //處理@Autowired注解        for(Field field:declaredFields){            Autowired autowired = field.getDeclaredAnnotation(Autowired.class);            if(autowired != null){                pvs.add(new PropertyValue(field.getName(),new BeanReference(BeanUtils.generateBeanName(field.getType().getName()),field.getType())));            }        }        //處理@Value注解        for(Field field:declaredFields){            Value value = field.getDeclaredAnnotation(Value.class);            if(value != null){                String value1 = value.value();                pvs.add(new PropertyValue(field.getName(),value1));            }        } 

字段值填充

獲取字段值后通過反射填入相應的字段中

  1. for(Field field:mbd.getBeanClass().getDeclaredFields()){                field.setAccessible(true);                if (field.getName().equals(propertiesValue.getName())&&field.getType().isAssignableFrom(newValue.getClass())) {                    field.set(bean,newValue);                }            } 

初始化

調用指定的初始化方法,進行資源的初始化.,如何獲取初始化方法?在xml模式中,只要加個標簽即可,如果是注解模式,加個注解標識一下或者在某個注解上加個參數,代表初始化方法,這個還沒有實現

功能填充

后置處理器添加

上面我們已經實現了一個可以進行依賴查找,依賴注入的Bean容器,讓我們再回顧一下Spring的流程,我們少了些什么,最容易想到的應該就是后置處理器了,包括BeanFactoryPostProcessor和BeanPostProcessor兩種,前者對于beanFactory進行修改操作,后者對于bean進行修改操作,同樣是面向接口編程

首先建立BeanPostProcessor

  1. public interface BeanPostProcessor {       Object postProcessBeforeInitialization(Object bean, String beanName);    Object postProcessAfterInitialization(Object bean, String beanName) ;} 

就目前來看,有什么是需要BeanPostProcessor來做的呢?我們可以把之前對注解進行處理,獲取注入屬性的代碼分離出來,專門用一個BeanPostProcessor去處理

所有自定義實現的BeanPostProcessor都需要繼承這個接口,由于BeanPostProcessor的作用是處理其他的Bean,所以必須要在其他被處理的Bean實例化之前被創建出來.于是我們在finishBeanFactoryInitialization(beanFactory);之前添加registerBeanPostProcessors(beanFactory);用于實例化所有的BeanPostProcessor

而這些beanPostProcessor的重要程度是不同的,例如處理注解注入的BeanPostProcessor優先級就要比一般的BeanPostProcessor優先級要高,所以需要先實例化

Aware接口添加

其實現在我們已經可以完全的把一個對象交由IOC容器了,但此時這個對象與容器之間的關系是單向的,容器能夠操作bean,但bean不能借助容器,為了解決此類問題,我們添加一個Aware接口作為標志接口,由各個更具體的Aware去繼承他,并在實例化屬性之后,初始化方法執行之完成相關容器屬性的注入

事件監聽器添加

監聽器是觀察者模式的一種實現

我們先定義以下幾個基本接口

  1. public interface ApplicationEventPublisher {    /**     * 發布事件     * @param event     */    void publishEvent(ApplicationEvent event);}public interface ApplicationEventMulticaster {    /**     * 添加廣播事件     * @param event     */    void multicastEvent(ApplicationEvent event);    /**     * 添加對于某個事件的監聽器     * @param listener     */    void addApplicationListener(ApplicationListener listener);    /**     * 移除指定監聽器     * @param listener     */    void removeApplicationListener(ApplicationListener listener);}public interface ApplicationListener <E extends ApplicationEvent> extends EventListener {    /**     * 監聽特定事件     * @param event     */    void onApplicationEvent(E event);} 

具體調用流程為具體的listener被添加到廣播器中,事件通過publisher統一發布,而publishEvent最后會調用 multicastEvent(ApplicationEvent event)方法,經過相應判斷后由對應監聽器做出相應操作.

如何判斷這個監聽器是否對該事件感興趣?

我們事先實現的listener是有泛型的,我們可以通過這個泛型與傳入的事件類型的關系來判斷

  1. public boolean supportEvent(ApplicationListener<ApplicationEvent> listener,ApplicationEvent event){        //先獲取Class對象        Class<? extends ApplicationListener> listenerClass = listener.getClass();        //獲取其實現的所有接口(包括泛型信息)        Type[] genericInterfaces = listenerClass.getGenericInterfaces();        for (Type genericInterface:genericInterfaces){            //判斷是否為泛型接口            if(genericInterface instanceof ParameterizedType){                ParameterizedType parameterizedType = (ParameterizedType) genericInterface;                //得到所有泛型參數                Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();                for(Type actualTypeArgument:actualTypeArguments){                    try {                        Class<?> aClass = Class.forName(actualTypeArgument.getTypeName());                        //判斷感興趣的事件類型是否與傳入事件相同,或者是其父類                        if(aClass.isAssignableFrom(event.getClass())){                            return true;                        }                    } catch (ClassNotFoundException e) {                        e.printStackTrace();                    }                }            }        }        return false;    } 

FactoryBean添加

目前的Bean都是由BeanFactory來產生的,

我們用FactoryBean接口來標識這個產生Bean的特殊的Bean

循環依賴的解決

循環依賴是指A依賴于B的同時B依賴于A,解決方法為實例化與初始化分離,如果只考慮一般情況的話用兩級緩存實際上就夠了,

代碼優化

實現簡易AOP

如果從正統的AOP開始的話,隨之而來的就是一堆概念,包括切點,通知一類

我們先看AOP要做什么

所以說AOP的核心就是動態代理,我們以Cglib為例來看看動態代理要怎么用

  1. Enhancer enhancer = new Enhancer();//1. 為哪個類進行代理        enhancer.setSuperclass(Buy.class);        enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {            //2. 為該類的哪個方法進行代理            if(method.getName().equals("buyOne")){                //3. 代理究竟要做什么                System.out.println("hello");            }            //4. 調用原有的對象             methodProxy.invokeSuper(o,objects);                     return o;        });//5. 產生代理后的對象        Buy o = (Buy)enhancer.create(); 

這就是動態代理最核心的功能,也是AOP的核心功能,AOP的最終目的是代碼5,即產生一個代理對象,把這個代理對象交給IOC去管理

而為了達成這個目的,AOP框架需要做好代碼1-4所需要做的事,一和二組合起來,成了JoinPoint,3叫做Advice,這兩個組合起來就叫做Advisor,可不可以不分這些種類,就全寫在一個或幾個類里,當然可以,Spring0.9就是這么做的,但發展到如今,早已采用了這種劃分方式.本項目也采用這種分類.

先從連接點說起,如何確定到底在哪里實現功能增強,無非是類與方法兩個層次;

我們先定義ClassFilter與MethodMacther兩個接口

  1. public interface ClassFilter {    /**     * 給定類型是否匹配     * @param clazz     * @return     */    boolean matches(Class< ? > clazz);}public interface MethodMatcher {    /**     * 對應類的對應方法是否匹配     * @param method     * @param targetClass     * @return     */    boolean matches(Method method,Class< ? > targetClass);} 

這兩個接口必然是組合起來使用的,于是我們用PointCut將其組合起來

  1. public interface Pointcut {    /**     * 獲取ClassFilter     * @return     */    ClassFilter getClassFilter();    /**     * 獲取MethodMatcher     * @return     */    MethodMatcher getMethodMatcher();} 

接口只是定義了抽象功能,這些功能還要有具體的實現

我們默認用Java的正則去匹配方法名,以此構建出JdkRegexMethodMatcher

  1. public class JdkRegexMethodPointcut implements MethodMatcher, Pointcut{    private Pattern[] compiledPatterns = new Pattern[0];    @Override    public ClassFilter getClassFilter() {        return null;    }    @Override    public MethodMatcher getMethodMatcher() {        return this;    }    @Override    public boolean matches(Method method, Class<?> targetClass) {        String name = method.getName();        for (Pattern pattern :compiledPatterns) {            Matcher matcher = pattern.matcher(name);            if(matcher.matches()){                return true;            }        }        return false;    }    //預編譯    private Pattern[] compilePatterns(String[] source) throws PatternSyntaxException {        Pattern[] destination = new Pattern[source.length];        for (int i = 0; i < source.length; i++) {            destination[i] = Pattern.compile(source[i]);        }        return destination;    }    public void initPatternRepresentation(String[] patterns) throws PatternSyntaxException {        this.compiledPatterns = compilePatterns(patterns);    }} 

在Spring中,并不是直接繼承的MethodMatcher,考慮到正則的語法不同,額外做了一層抽象,但在此處省略掉了

而JdkRegexMethodMatcher同時也實現了PointCut類,也就是說,現在切點已經準備好了

再來看Advice

由于考慮的可擴展點比較多,于是繼承的層次也變的多了

  1. public interface Advice {}public interface BeforeAdvice extends Advice{}public interface MethodBeforeAdvice extends BeforeAdvice{        void before(Method method, Object[] args, Object target) throws Throwable;} 

現在Advice也定義完了,具體的實現我們交由用戶去做

接下來就是整合成Advisor了

  1. public interface Advisor {    Advice getAdvice();}public interface PointcutAdvisor extends Advisor{       Pointcut getPointcut();}public abstract class AbstractPointcutAdvisor implements PointcutAdvisor{    private Advice advice;    @Override    public Advice getAdvice() {        return advice;    }    public void setAdvice(Advice advice) {        this.advice = advice;    }} 

目前已經定義好了Advisor的功能

我們再實現這個接口

  1. public class RegexMethodPointcutAdvisor extends AbstractPointcutAdvisor {    JdkRegexMethodPointcut pointcut = new JdkRegexMethodPointcut();    private String[] patterns;    public RegexMethodPointcutAdvisor() {    }    public RegexMethodPointcutAdvisor(Advice advice) {        setAdvice(advice);    }    public void setPattern(String pattern) {        setPatterns(pattern);    }    public void setPatterns(String... patterns) {        this.patterns = patterns;        pointcut.initPatternRepresentation(patterns);    }    @Override    public Pointcut getPointcut() {        return pointcut;    }} 

RegexMethodPointcutAdvisor就整合了PointCut以及Advice,通過他,我們就可以確定在何處做何種增強.

現在的advisor可以完成檢驗一個類是否要被代理的功能,但是如果這個類需要被代理,advisor卻無法保存這個類的對應信息

于是我們需要一個類將advisor與對應的代理類結合起來,這就是AdvisedSupport

  1. public class AdvisedSupport {    private  TargetSource targetSource;    private List<MethodInterceptor> methodInterceptors = new ArrayList<>();    private List<PointcutAdvisor> advisors = new ArrayList<>();    public TargetSource getTargetSource() {        return targetSource;    }    public void setTargetSource(TargetSource targetSource) {        this.targetSource = targetSource;    }    public List<MethodInterceptor> getMethodInterceptor() {        return methodInterceptors;    }    public void addMethodInterceptor(MethodInterceptor methodInterceptor) {        this.methodInterceptors.add(methodInterceptor);    }    public List<PointcutAdvisor> getAdvisor() {        return advisors;    }    public void addAdvisor(PointcutAdvisor advisor) {        MethodBeforeAdviceInterceptor methodBeforeAdviceInterceptor = new MethodBeforeAdviceInterceptor();        methodBeforeAdviceInterceptor.setAdvice((MethodBeforeAdvice) advisor.getAdvice());        addMethodInterceptor(methodBeforeAdviceInterceptor);        this.advisors.add(advisor);    }} 

上類屬性中的TargetSource便是真正持有代理對象信息的類

現在萬事具備,只需要用Cglib去使用我們已經持有的信息就可以創建出新的類了

  1. public class CglibAopProxy implements AopProxy{    private final AdvisedSupport advised;    public CglibAopProxy(AdvisedSupport advised) {        this.advised = advised;    }    @Override    public Object getProxy() {        Enhancer enhancer = new Enhancer();        //1. 為哪個類進行代理        enhancer.setSuperclass(advised.getTargetSource().getTargetClass());        enhancer.setCallback(new DynamicAdvisedInterceptor(advised));        //5. 產生代理后的對象        return enhancer.create();    }    private static class DynamicAdvisedInterceptor implements MethodInterceptor {        private final AdvisedSupport advised;        public DynamicAdvisedInterceptor(AdvisedSupport advised) {            this.advised = advised;        }        @Override        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {            CglibInvocation cglibInvocation = new CglibInvocation(method,objects,o,methodProxy);            //2. 為該類的哪個方法進行代理            for(PointcutAdvisor advisor: advised.getAdvisor()){                if(advisor.getPointcut().getMethodMatcher().matches(method,advised.getTargetSource().getTargetClass())){                    //3. 代理究竟要做什么                    return advised.getMethodInterceptor().get(0).invoke(cglibInvocation);                }            }            //4. 調用源方法            return cglibInvocation.proceed();        }    }} 

將這份代碼與最初使用cglib的代碼比較,會發現過程幾乎是一模一樣.但是作為一個框架,應該盡可能的給用戶以方便

于是我們需要一個Creator去把這一切都做好,他需要負責將Advice和PointCut組合成Advisor,再將Advisor與TargetSource組裝成AdvisedSupport,再將AdvisedSupport交給Cglib動態代理,產生代理對象,而用戶只需要編寫Advice以及切入點表達式即可

功能演示

屬性注入基本類型引用類型循環依賴
容器感知
FactoryBean生成對象
AOP切面增強
自定義BeanPostProcessor

困難及解決

首先是設計上的問題
FactoryBean的實現
AOP與IOC的結合
字段的注入

責任編輯:梁菲 來源: 阿里云云棲號
相關推薦

2021-02-17 09:39:41

PodmanDockerLinux

2011-03-24 09:34:41

SPRING

2015-12-30 14:50:45

Kubernetes容器技術Docker

2022-09-22 16:21:43

開源GUI 應用

2023-11-28 13:50:00

Kubernetes容器

2019-01-11 13:57:06

2021-05-20 07:56:35

Bean容器Spring

2019-12-09 15:00:48

TomcatServlet容器

2025-03-21 08:30:00

容器管理開發開源

2025-01-03 09:00:00

代碼C++gTest

2019-04-09 08:50:15

Rancher容器運維

2023-05-15 13:59:53

紅帽容器原生虛擬化KubeVirt

2013-07-02 10:24:52

團隊管理團隊遠程團隊

2020-11-13 07:08:51

Spring Boot應用Spring

2023-10-18 15:25:29

數據源數據庫

2022-01-26 15:20:00

配置微服務架構

2018-05-08 08:35:34

LinuxDocker 容器管理器

2020-09-24 11:46:03

Promise

2022-07-13 15:31:29

手繪板canvas鴻蒙

2022-08-02 14:21:20

滑動驗證碼鴻蒙
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品久久久久久二区 | yeyeav| aⅴ色国产 欧美 | 亚洲一区久久 | av激情影院 | 精品免费av | 成人av免费 | 成人影 | 国产小视频精品 | 九一在线观看 | www.日韩在线 | 日韩精品视频一区二区三区 | 羞羞视频网站 | 中文字幕综合 | 自拍亚洲| 综合久久亚洲 | 午夜小视频免费观看 | 97伦理电影网 | 久久精品亚洲精品国产欧美 | 久久99一区二区 | 中文字幕蜜臀av | 亚洲一区二区三区福利 | 男人的天堂中文字幕 | 不卡av电影在线播放 | 久久中文字幕一区 | 91精品久久久久久久久99蜜臂 | 欧美一区二区三区在线看 | 色婷婷亚洲国产女人的天堂 | 成年人免费看 | 亚洲品质自拍视频网站 | 中文字幕免费观看 | 国产精品久久久久久久免费大片 | 日本在线你懂的 | 国产精品乱码一区二区三区 | 国产精品国产自产拍高清 | 久久久久久看片 | 亚洲欧美在线视频 | 中文字幕一区二区三区日韩精品 | 国产精品久久国产精品 | 蜜桃视频在线观看免费视频网站www | 国产国拍亚洲精品av |