I want to share with you my experience of studying the AOP paradigm and developing with its use in a large project.

Aspect-oriented programming, or AOP, is a paradigm that distinguishes end-to-end functionality and isolates it in the form of a so-called aspect, or aspect class. This implies the presence of semantic tools and mechanisms for the engine compartment injection of an aspect into the application code. Thus, it turns out that the aspect itself determines which parts of the application it needs to process, while the application does not know (before compilation, of course) that an alien code is impudently and shamelessly entered into its sections.
')
Suppose that we have a rather trivial task - to provide the application with support for some languages (russian, english, italian, french, etc.). You will say that we have a linguistic and regional differentiation of all resources, and you will be right. Except for the case when the application uses not the built-in resources, but “pulls” them from the server. In general, this situation is often encountered and is solved trivially - we add BaseSctivity to the abstract class, which we inherit from the system class, a couple of lines to the handler, and everything works. And you can do without these pair of lines. And even without a base class. And if necessary, just copy one file to the application or add a dependency in the gradle, which will do everything itself.
So, the task is clear, we write.
package com.archinamon.example.xpoint; import android.support.v7.app.AppCompatActivity; import android.app.Application; public aspect LocaleMonitor { pointcut saveLocale(): execution(* MyApplication.onCreate()); pointcut checkLocale(AppCompatActivity activity): this(activity) && execution(* AppCompatActivity+.onCreate(..)); after(): saveLocale() { saveCurrentLocale(); } before(AppCompatActivity activity): checkLocale(activity) { if (isLocaleChanged()) { saveCurrentLocale(); restartApplication(activity); } } void saveCurrentLocale() {} void restartApplication(AppCompatActivity context) {} boolean isLocaleChanged() {} }
By adding this class to our application, we will teach it to restart itself when changing the language in the system.
For a while I searched for ready-made solutions and tasted them. As a result, he wrote his own version. How it all works, why I had to reinvent the wheel and what came of it all - under the cut!
It should be noted that only the general framework of the aspect is described above, all the mechanics make you add a few extra lines of
tips to take context data from
slices and
connection points . You will find the full aspect code in the demo application at the end of the article. The example illustrates the compactness and brevity of injections, due to which the aspect class permeates our application.
Philosophy
From this reservation it follows that the aspect class is a decorator over an object class. Approaching the task of implementing some functionality on aspects, it is important not to forget about the object-oriented mechanisms of language and ideology as such. Bad way - when an aspect becomes a class that is unreadable and stuffed with utility tools. Best way - to describe all the logic with a separate object module, as isolated as possible from the outside world, which is connected to the application through an aspect decorator.
Speaking about the aspect approach to development as a kind of decorating mechanism, it should be noted its main features, which were partially demonstrated in the example. The notion of procedures, functions, and methods is replaced by the term
“advice” . Advice can be applied
before (
before ),
after (
after ) or
instead of (
around ) that part of the code where we have been inserted through the
slice . In turn, the notion of a
slice (
pointcut ) hides under itself some description of the point (ek) in our program where the aspect class is connected. This description is a whole set of parameters - the name of the class and / or the signature of the method, the place where it was called or executed, etc. One slice can describe at least one
join point (
joinPoint ), but the maximum is not limited and can permeate the entire application.
Testing
Another area where aspects can manifest itself in all its glory is testing and debugging. Writing a universal profiler of methods and classes is easier than it might seem.
MyProfilerImpl extends Profiler aspect package com.archinamon.example.xpoint; aspect MyProfilerImpl extends Profiler { private pointcut strict(): within(com.archinamon.example.*) && !within(*.xpoint.*); pointcut innerExecution(): strict() && execution(!public !static * *(..)); pointcut constructorCall(): strict() && call(*.new(..)); pointcut publicExecution(): strict() && execution(public !static * *(..)); pointcut staticsOnly(): strict() && execution(static * *(..)); private pointcut catchAny(): innerExecution() || constructorCall() || publicExecution() || staticsOnly(); before(): catchAny() { writeEnterTime(thisJoinPointStaticPart); } after(): catchAny() { writeExitTime(thisJoinPointStaticPart); } } abstract aspect Profiler issingleton() { abstract pointcut innerExecution(); abstract pointcut constructorCall(); abstract pointcut publicExecution(); abstract pointcut staticsOnly(); protected static final Map<String, Long> sTimeData = new ConcurrentHashMap<>(); protected void writeEnterTime(JoinPoint.StaticPart jp) {} protected void writeExitTime(JoinPoint.StaticPart jp) {} }
The
strict () slice cuts the crawl of junction points to avoid traversing the aspect classes themselves. Otherwise, the structure described is extremely simple and intuitive. I deliberately divide the selection of methods, constructors, and static methods into different cuts — this will allow you to flexibly customize the profiler for a specific application and a specific task. The
issingleton () marker in the description of an abstract aspect class explicitly declares that each successor will be a singleton. In fact, the record is unnecessary, because all aspect classes are singleton by default. In our case, this marker is needed here to inform about this property of a third-party developer. In my practice, I prefer to label implicit functionality - thus increasing the understanding and readability of the module for others.
We proceed directly to the testing. What aspects are effective?
- First of all, its internal mechanics. We are testing a section of code in its usual habitat, and not trying to emulate the context of application of a specific functional (functions, procedures, the object itself, etc.).
- The second important plus is the wealth of debug information available at the connection point due to injections at the compilation stage.
- The third and very important plus lies in the strength of the so-called NamePattern, which describes all the parameters of the slice. A name pattern can cover a huge number of similar and similar areas (for example, capture all setters getters with a single line cut).
All this gives a great profit when writing unit and functional tests. But there is always an important "but." Aspect testing is more likely a real-time analysis. To implement the classic testing cycle, it is still necessary to describe some environment or context in which the test decorators will be executed. So, as a replacement, the usual frameworks of AOP will not work.
I summarize all of the above. Aspect approach is well suited as an addition to classical testing to cover analyzers and monitors of the work product in order to detect errors in an already running application. For example, with manual testing or as a beta version of the application.
PS Useful stuffAnd on aspects you can cover classes and / or methods with exception handlers with one swipe of your fingers: abstract aspect NetworkProtector { abstract pointcut myClass(); Response around(): myClass() && execution(* executeRequest(..)) { try { return proceed(); } catch (NetworkException ex) { Response response = new Response(); response.addError(new Error(ex)); return response; } } }
This is the easiest option, and it can be more difficult, describing all the stages in detail and in steps. Bicycle v3.14: Why and How?
You can master AOP without Android. To use technology in my project, I started writing my own plug-in to the Gradle build system. But there are already ready-made solutions, the well-informed reader will say! And he will be right. And it will not be completely right, because all available only for a narrow range of conditions, but did not cover all the necessary and possible combinations. For example, no plugin could work correctly with flavors or create its own source-set for source code decomposition. And some allowed to write aspects only in the style of java-annotations. So I collected all the rakes on the way to implement your own plugin. Which, as a result, covered all the bottlenecks.
And I even got a hacked semantic plugin (hello Spring @ AOP from Ultimate version!) For personal convenience.We are waiting for the official release in future versions of Android StudioIssue Title:
AspectJ supportLabels: Type-Enhancement
Subcomponent-Tools-Studio Subcomponent-Tools-gradle-ide
Priority-Small Target-1.6
I note a few obvious and not so things that I had to face during development.
Organization of compiler task stackHere and support preprocessing tools, and Retrolambda brought a headache. It was the first and, subjectively, the most difficult rake. I first took to writing an extension for Gradle, and I collected all the pitfalls with stack management at the very first stage of development. According to the results, the plug-in explicitly checks the project for connecting Retrolambda to it and, if the result is positive, it is embedded in the queue before its task. Otherwise, it becomes in the queue immediately after the java-compiler.
Performance and incremental buildProperly organizing a stack of tasks is half the battle. To optimize it and give odds to performance is a task of another level. AspectJ connects with its own compiler, ajc. And so everything is handled. Frankly, there is still room for a plugin to develop. The tasks of code generation by preprocessor, java-source builds, assembly of dex-files (Android executable files of the environment) work in usual optimized conditions. However, ajc still does not work in incremental mode.
Adaptation of workspace and gradle-toolsAt first I implemented the basic features and support for popular plugins. The source code is compiled, aspects are embedded, the application is being built. In the project, which became a test site, a terrible date for the introduction of flavors came up. I understood that the whole plugin will become irrelevant if it fails to make friends with these tools. And at the same time I wanted to conveniently and concisely equip the workspace with the source code. Soon, the plugin learned how to work with build-options, properly integrate into their tasks. And at the end I got my own folder in resources - aspectj, along with java, groovy, aidl and others.
What to take with you?
To the heap to aspects, we also take the syntax of Java 8 and StreamAPI from the same eight (this is necessary for functional work with arrays, collections and lists) - after all, the Java API is already included in the Android API, and alas, it does not boast of innovations.
The build.gradle project file is transformed. buildscript { repositories { mavenCentral() maven { url 'https://raw.github.com/Archinamon/GradleAspectJ-Android/master/' } maven { url 'https://raw.github.com/Archinamon/RetroStream/master/' } } dependencies { //retrolambda classpath 'me.tatarka:gradle-retrolambda:3.2.3' //aspectj classpath 'com.archinamon:AspectJ-gradle:1.0.16' } } // Required because retrolambda is on maven central repositories { mavenCentral() } apply plugin: 'com.android.application' //or apply plugin: 'com.android.library' apply plugin: 'me.tatarka.retrolambda' apply plugin: 'com.archinamon.aspectj' dependencies { compile 'com.archinamon:RetroStream:1.0.4' }
That's all! I will leave the detailed setting behind the scenes, but all the details can be found in the source code on the github.
Links
Demonstration
project .
Gradle plugin for Android Studio.