То, чем я хочу поделиться с вами сегодня, это АОП (аспектно-ориентированное программирование).Название находится всего в одной букве от ООП.По сути, это дополнение к ООП-программированию, а не замена. Перевод "аспектно-ориентированное программирование", но я предпочитаю переводить как "аспектно-ориентированное программирование". Звучит немного загадочно,Зачем? Когда вы прочитаете эту статью, вы поймете, что очень важная работа, которую мы делаем, — это написание этого «раздела». Так что же такое «резать»?
Верно! Просто используйте нож, чтобы разрезать кусок лапши. Обратите внимание, что по отношению к грани мы должны разрезать ее сбоку, что для краткости называется «поперечным разрезом». Вы можете представить кусок кода в виде куска лапши, а также можете использовать нож, чтобы разрезать его поперек, Следующее, что нужно сделать, это как реализовать этот нож!
Чтобы было ясно, это понятие не было введено Родом Джонсоном. На самом деле он существовал давно, самый известный и самый мощный Java-проект с открытым исходным кодом — AspectJ, но его предшественник — AspectWerkz (проект перестал обновляться в 2005 году), который является предком АОП. Лао Луо (гений с лысыми волосами и мой отец) написал фреймворк под названием Spring, который мгновенно стал хитом и стал отцом Spring. На базе своего IOC он реализовал набор АОП фреймворка.Потом он как будто все больше и больше попадал в пропасть.Когда он уже не мог выбраться,кто-то предложил ему интегрировать AspectJ.Он был беспомощен. предложение было принято ниже. Поэтому фреймворк АОП, который мы сейчас используем больше всего, должен быть Spring + AspectJ.
Так что же такое АОП? Как это использовать? Эта статья постепенно перенесет вас в мир АОП, благодаря чему вы почувствуете себя как никогда комфортно!
Но прежде чем я перейду к АОП, думаю, стоит вспомнить этот код:
1. Мертвый код
Сначала интерфейс:
public interface Greeting {
void sayHello(String name);
}
Существует также класс реализации:
public class GreetingImpl implements Greeting {
@Override
public void sayHello(String name) {
before();
System.out.println("Hello! " + name);
after();
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
Методы before() и after() встроены в тело метода sayHello(), и этот код очень плохо пахнет. Если какой-нибудь мужчина напишет много такого кода, ваш архитектор обязательно его отругает.
Например: Мы хотим посчитать время выполнения каждого метода, чтобы оценить производительность. Нужно ли что-то делать в начале и в конце каждого метода?
Другой пример: если мы хотим написать программу JDBC, должны ли мы подключаться к базе данных в начале метода и закрывать соединение с базой данных в конце метода?
Такой код только утомит программистов и разозлит архитекторов!
Обязательно найдите способ рефакторинга приведенного выше кода, сначала дайте три решения:
2. Статический прокси
Самое простое решение — использовать режим статического прокси, для класса GreetingImpl пишем отдельный прокси-класс:
public class GreetingProxy implements Greeting {
private GreetingImpl greetingImpl;
public GreetingProxy(GreetingImpl greetingImpl) {
this.greetingImpl = greetingImpl;
}
@Override
public void sayHello(String name) {
before();
greetingImpl.sayHello(name);
after();
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
Просто используйте этот GreetingProxy для проксирования GreetingImpl, давайте посмотрим, как его вызывает клиент:
public class Client {
public static void main(String[] args) {
Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
greetingProxy.sayHello("Jack");
}
}
Это правильно, но есть проблема.Таких классов, как XxxProxy, будет все больше и больше.Как мы можем максимально сократить эти классы прокси? Желательно только один класс прокси.
Затем нам нужно использовать динамический прокси, предоставляемый JDK.
3. Динамический прокси JDK
public class JDKDynamicProxy implements InvocationHandler {
private Object target;
public JDKDynamicProxy(Object target) {
this.target = target;
}
@SuppressWarnings("unchecked")
public <T> T getProxy() {
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
Клиент вызывается так:
public class Client {
public static void main(String[] args) {
Greeting greeting = new JDKDynamicProxy(new GreetingImpl()).getProxy();
greeting.sayHello("Jack");
}
}
Таким образом, все прокси-классы объединяются в динамический прокси-класс, но проблема остается: динамический прокси, предоставляемый JDK, может проксировать только интерфейс, но не класс без интерфейса. Есть ли способ решить эту проблему?
4. Динамический прокси CGLib
Мы используем библиотеку классов CGLib с открытым исходным кодом для проксирования классов без интерфейсов, что компенсирует недостатки JDK. Класс динамического прокси CGLib работает следующим образом:
public class CGLibDynamicProxy implements MethodInterceptor {
private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
private CGLibDynamicProxy() {
}
public static CGLibDynamicProxy getInstance() {
return instance;
}
@SuppressWarnings("unchecked")
public <T> T getProxy(Class<T> cls) {
return (T) Enhancer.create(cls, this);
}
@Override
public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object result = proxy.invokeSuper(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
В приведенном выше коде используется режим Singleton, поэтому вызов клиента упрощается:
public class Client {
public static void main(String[] args) {
Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class);
greeting.sayHello("Jack");
}
}
На данный момент мы сделали все, что могли, и проблема, кажется, решена. Но не всегда все так идеально, и надо стремиться к совершенству!
Лао Луо придумал фреймворк АОП, может ли он быть совершенным и элегантным? Пожалуйста, продолжайте читать ниже!
5. Spring AOP: Front Enhancement, Post Enhancement, Surround Enhancement (программно)
В мире Spring AOP слишком много терминов, связанных с АОП, которые часто являются нашими «камнями преткновения» Будь то чтение книги или технической документации, эти термины необходимо внушать читателю один за другим в начале . Я думаю, что это совершенно пугает, это не так сложно, давайте успокоимся.
Метод before(), упомянутый в нашем примере выше, вызывается в Spring AOP.Перед Советом.Некоторые люди буквально переводят Advice как «уведомление», что я считаю неуместным, поскольку оно означает вовсе не «уведомление», а «улучшение» функциональности исходного кода. Кроме того, в CGLib также есть класс Enhancer, который является классом расширения.
Кроме того, вызываются такие методы, как after().после консультации, потому что он размещен позже для расширения функциональности кода.
Если вы можете объединить before() и after() вместе, тогда вызовитеВокруг Совет, как гамбургер с ветчиной посередине.
Легко ли понять эти три понятия? Если да, то вперед!
Что нам нужно сделать дальше, так это реализовать эти так называемые «классы расширения» и позволить им пересекаться с кодом, а не жестко прописывать их в коде.
Начнем с класса предварительного улучшения:
public class GreetingBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("Before");
}
}
Примечание. Этот класс реализует интерфейс org.springframework.aop.MethodBeforeAdvice, и мы помещаем в него код, который необходимо улучшить.
Еще один пост-улучшенный класс:
public class GreetingAfterAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object result, Method method, Object[] args, Object target) throws Throwable {
System.out.println("After");
}
}
Точно так же этот класс реализует интерфейс org.springframework.aop.AfterReturningAdvice.
Наконец, используйте клиент для их интеграции, посмотрите, как это вызвать:
public class Client {
public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory(); // 创建代理工厂
proxyFactory.setTarget(new GreetingImpl()); // 射入目标类对象
proxyFactory.addAdvice(new GreetingBeforeAdvice()); // 添加前置增强
proxyFactory.addAdvice(new GreetingAfterAdvice()); // 添加后置增强
Greeting greeting = (Greeting) proxyFactory.getProxy(); // 从代理工厂中获取代理
greeting.sayHello("Jack"); // 调用代理的方法
}
}
Пожалуйста, внимательно прочитайте приведенный выше код и его комментарии, вы обнаружите, что Spring AOP на самом деле довольно прост, верно?
Конечно, мы можем определить только один расширенный класс и позволить ему реализовывать интерфейсы MethodBeforeAdvice и AfterReturningAdvice следующим образом:
public class GreetingBeforeAndAfterAdvice implements MethodBeforeAdvice, AfterReturningAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("Before");
}
@Override
public void afterReturning(Object result, Method method, Object[] args, Object target) throws Throwable {
System.out.println("After");
}
}
Таким образом, нам нужно использовать только одну строку кода, и мы можем одновременно добавить пре- и пост-улучшение:
proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice());
Я только что упомянул «улучшение объемного звучания», на самом деле эта штука может совмещать функции «предварительного улучшения» и «пост-улучшения», не требуя от нас одновременной реализации двух вышеупомянутых интерфейсов.
public class GreetingAroundAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
before();
Object result = invocation.proceed();
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
Классы улучшения объемного звучания должны реализовать интерфейс org.aopalliance.intercept.MethodInterceptor. Обратите внимание, что этот интерфейс не предоставляется Spring, он написан консорциумом АОП (очень крутой консорциум), и Spring просто его заимствует.
В клиенте объект расширенного класса также нужно добавить в фабрику прокси:
proxyFactory.addAdvice(new GreetingAroundAdvice());
Ну, это основное использование Spring AOP, но оно просто «программное». Если бы Spring AOP был именно таким, это было бы глупо, ведь когда-то он рекламировал использование конфигурационных файлов Spring для определения bean-объектов и освободил все новые операции в коде.
6. Spring AOP: предварительное улучшение, после улучшения, улучшение объемного звучания (декларативное)
Давайте сначала посмотрим, как написан файл конфигурации Spring:
<?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/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 扫描指定包(将 @Component 注解的类自动定义为 Spring Bean) -->
<context:component-scan base-package="aop.demo"/>
<!-- 配置一个代理 -->
<bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interfaces" value="aop.Greeting"/> <!-- 需要代理的接口 -->
<property name="target" ref="greetingImpl"/> <!-- 接口实现类 -->
<property name="interceptorNames"> <!-- 拦截器名称(也就是增强类名称,Spring Bean 的 id) -->
<list>
<value>greetingAroundAdvice</value>
</list>
</property>
</bean>
</beans>
Обязательно прочитайте комментарии к приведенному выше коду.На самом деле использование ProxyFactoryBean может заменить предыдущий ProxyFactory.По сути, это одно и то же. Я думаю, может быть проще понять, что interceptorNames следует переименовать в советаNames , разве это не просто добавить класс расширения к этому свойству?
Кроме того, если имеется только один класс расширения, для упрощения можно использовать следующее:
...
<bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interfaces" value="aop.Greeting"/>
<property name="target" ref="greetingImpl"/>
<property name="interceptorNames" value="greetingAroundAdvice"/> <!-- 注意这行配置 -->
</bean>
...
Также следует отметить, что здесь используется функция Spring 2.5+ «Сканирование компонентов», поэтому нам не нужно постоянно определять
Смотри как это легко:
@Component
public class GreetingImpl implements Greeting {
...
}
@Component
public class GreetingAroundAdvice implements MethodInterceptor {
...
}
Наконец, посмотрите на клиента:
public class Client {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("aop/demo/spring.xml"); // 获取 Spring Context
Greeting greeting = (Greeting) context.getBean("greetingProxy"); // 从 Context 中根据 id 获取 Bean 对象(其实就是一个代理)
greeting.sayHello("Jack"); // 调用代理的方法
}
}
Кода очень мало, мы помещаем код конфигурации в файл конфигурации, что также полезно для постобслуживания. Что еще более важно, код фокусируется только на бизнес-логике и помещает конфигурацию в файлы. Это лучшая практика!
В дополнение к трем типам улучшений, упомянутых выше, на самом деле есть два типа улучшений, которые необходимо понимать, и вы должны иметь возможность думать о них, когда это необходимо.
7. Spring AOP: метательное улучшение
Программа сообщает об ошибке и выдает исключение.Общая практика - выводить в консоль или лог-файл, так много мест надо обработать.Есть ли способ сделать это раз и навсегда? Это Броски Совет, это действительно сильно, если вы мне не верите, просто продолжайте читать:
@Component
public class GreetingImpl implements Greeting {
@Override
public void sayHello(String name) {
System.out.println("Hello! " + name);
throw new RuntimeException("Error"); // 故意抛出一个异常,看看异常信息能否被拦截到
}
}
Вот код, который создает расширенный класс:
@Component
public class GreetingThrowAdvice implements ThrowsAdvice {
public void afterThrowing(Method method, Object[] args, Object target, Exception e) {
System.out.println("---------- Throw Exception ----------");
System.out.println("Target Class: " + target.getClass().getName());
System.out.println("Method Name: " + method.getName());
System.out.println("Exception Message: " + e.getMessage());
System.out.println("-------------------------------------");
}
}
Классы расширения бросания должны реализовать интерфейс org.springframework.aop.ThrowsAdvice, в котором можно получить такую информацию, как методы, параметры, целевые объекты и объекты исключений. Мы можем единообразно записывать эту информацию в журнал, и, конечно же, ее также можно сохранять в базе данных.
Эта функция действительно потрясающая! Но есть еще одно мощное усиление. Если класс реализует интерфейс A, но не интерфейс B, может ли класс вызывать методы интерфейса B? Если вы не видите, что внизу, вы не можете поверить, что это действительно возможно!
8. Spring AOP: Представляем улучшения
Вышеупомянутое — это все усовершенствования методов, так можно ли улучшать классы? На жаргоне АОП расширение метода называетсяТкачество, а расширение класса называетсяВведение. и Введение СоветЭто усовершенствование класса, а также последнее усовершенствование, предоставляемое Spring AOP. Рекомендуется вообще не читать «Справочник по весне», иначе вы пожалеете об этом. Потому что, когда вы посмотрите на пример кода ниже, вы обязательно поймете, что такое вводное улучшение.
Определен новый интерфейс Apology (извинения):
public interface Apology {
void saySorry(String name);
}
Но я не хочу, чтобы GreetingImpl реализовывал этот интерфейс прямо в коде, я хочу реализовать его динамически во время работы программы. Потому что если я реализую этот интерфейс, то я должен переписать класс GreetingImpl. Главное, что я не хочу его менять. Может быть, в реальном сценарии этот класс имеет 10 000 строк кода, и я действительно не осмеливаюсь его перемещать. Итак, мне нужно использовать введение Spring для улучшения. Это типа интересно!
@Component
public class GreetingIntroAdvice extends DelegatingIntroductionInterceptor implements Apology {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
return super.invoke(invocation);
}
@Override
public void saySorry(String name) {
System.out.println("Sorry! " + name);
}
}
Приведенное выше определяет класс улучшения введения, который расширяет класс org.springframework.aop.support.DelegatingIntroductionInterceptor, а также реализует недавно определенный интерфейс Apology. Класс сначала переопределяет метод invoke() родительского класса, а затем реализует методы интерфейса Apology. Я просто хочу использовать этот расширенный класс для расширения функций класса GreetingImpl, чтобы этот класс GreetingImpl мог вызывать методы интерфейса Apology во время работы программы без непосредственной реализации интерфейса Apology. Это просто потрясающе!
Посмотрим, как он настроен:
<?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/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="aop.demo"/>
<bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interfaces" value="aop.demo.Apology"/> <!-- 需要动态实现的接口 -->
<property name="target" ref="greetingImpl"/> <!-- 目标类 -->
<property name="interceptorNames" value="greetingIntroAdvice"/> <!-- 引入增强 -->
<property name="proxyTargetClass" value="true"/> <!-- 代理目标类(默认为 false,代理接口) -->
</bean>
</beans>
Необходимо обратить внимание на атрибут proxyTargetClass, который указывает, следует ли проксировать целевой класс. Значение по умолчанию — false, что является прокси-интерфейсом. В настоящее время Spring использует динамический прокси JDK. Если это правда, то Spring использует динамический прокси CGLib. Это просто так удобно! Spring инкапсулирует все это, так что программистам не нужно столько деталей. Мы хотим отдать дань уважения товарищу Лао Луо, ты наш кумир навсегда!
Все вышеперечисленное будет полностью понято, когда вы прочитаете клиентский код ниже:
public class Client {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("aop/demo/spring.xml");
GreetingImpl greetingImpl = (GreetingImpl) context.getBean("greetingProxy"); // 注意:转型为目标类,而并非它的 Greeting 接口
greetingImpl.sayHello("Jack");
Apology apology = (Apology) greetingImpl; // 将目标类强制向上转型为 Apology 接口(这是引入增强给我们带来的特性,也就是“接口动态实现”功能)
apology.saySorry("Jack");
}
}
Неожиданно метод saySorry() может быть вызван непосредственно объектом GreetingImpl, просто приведите его к этому интерфейсу.
Мы еще раз благодарим Spring AOP и благодарим Лао Луо за предоставление нам таких мощных функций!
На самом деле, в Spring AOP еще есть много замечательных мест.В следующей статье будут представлены все более и более ценные технологии AOP, чтобы каждый мог получить больше преимуществ.
не закончен,продолжение следует...
Обратите внимание на публичный аккаунт WeChat【мечта программиста], уделяя особое внимание технологиям с полным стеком, таким как Java, SpringBoot, SpringCloud, микросервисам, Docker, а также разделению интерфейсной и серверной частей.