Aspect-Oriented Programming
en.wikipedia.org
en.wikipedia.org
Admittedly, IDEs were pretty rudimentary when AOP was first introduced, and today's IDEs can display aspects right where they are applied. But I still think that annotations/decorators are a far better solution for the problem that AOP tried to solve.
I think in the end things like Python decorators follow similar ideas without the problems associated. No magic, just functions.
I do like that AOP helped "Cross Cutting Concern" become a common thing in development vocabulary, though.
AOP makes Java style logging incredibly convenient. That was not only its main selling point, but also it's one single benefit that people managed to identify during the few years it was hyped.
IMO, Java style logging is bad by itself. But surely, AOP does make Java style logging much more convenient.
At this point we're reaching limits of IDE support[0] and language design. New languages have been piling up increasingly arcane mathematical abstractions to try and squeeze in a lil' bit more stuff in the same number of characters, but they too can't get past the fundamental problem: there is only so much you can fit in a piece of plaintext. You can't cover every cross-cutting concern at the same time in the same piece of code, and keep all of them easy to understand and work with. That is, we are being limited by insisting on directly authoring and manipulating source code that's plaintext and a "single source of truth".
AOP solved a lot of these issues, but it was hamstrung by being deeply embedded in that "plaintext single source of truth" code workflow. It was confusing to have "spooky action at a distance" that's invisible unless you're using a properly configured IDE, but the real problem here is that we expect everything to be local in the same piece of text. If we drop that expectation - learn to work on higher-level representations of the underlying source code - then none of it is a problem anymore. AOP becomes a subset of more general techniques of authoring software systems.
--
[0] - Perhaps with exception of LLMs, which promise to be able to both write heaps of code and explain what all that code does. We can probably ride quite far in that direction, but it'll also eventually hit the same wall: no more progress until we abandon the idea of working with the plaintext source code directly.
This statement seems slightly ironic given the prevalence of IoC containers in the most commonly used backend languages and rampant 'interface everything' syndrome alongside it. You'd almost think such a world would embrace AOP.
The most commonly used languages are there because the most conservative people (that hire a large majority of people) pick them.
In the most basic form - passing dependencies to a function/object constructor, instead of instantiating them within said function/object constructor - I feel it's mostly a good idea, but there's something that's bugging me about this, and I'm not yet able to pinpoint what that is.
The more advanced form - autowiring things with magic annotations - yeah, I hated that when I was working in Java. However, these days I think the problem isn't with the idea, but - again - with our insistence on working with the final, pre-binary, single source of truth plaintext code. That is, autowiring is the kind of stuff that really needs support from tooling able to tell you what exactly happens in a particular scenario - it feels icky, but it does so because we're not used to relying on tooling over raw plaintext. And what I'm saying is, we should ditch the plaintext and embrace the tooling, or else we'll remain forever stuck in endless debates about code style, readability, etc. - problems that, to me, seem to be fundamentally caused by hard limits on how many cross-cutting concerns you can cram into a single block of text.
Well, I have seen some heat specifically against the habit popular on Java and C# of passing dependencies into factories and getting classes with completely different interfaces as a result.
But you are correct, IoC is a very generic name that can cover even passing lambdas to a map function.
Anyway, the problem with deviating from the single source of truth plaintext is that when other sources of truth have bugs, you are usually forever unable to fix them¹. This is because when people create tools that affect the program behavior independently of your source, they inevitably make them stateful. A stateless tool could in theory not have this problem, but then, developers are annoyingly good into reducing your stateless tool "configuration script" into normal code and using that instead.
1 - There are many programing environments where tools affect the program behavior. Those are usually aimed into novices and non-programmers, because programmers with any experience are quick to reject them. A very common theme on those environments is that error that keeps happening despite everything being correct, and can only be solved by throwing your work away and doing everything again exactly the same way from scratch.
In other words, you saw a bug in a software implementation.
What leads you to look at a bug and from that lay broad and overreaching personal claims about fundamental design patterns?
I have no idea where you got that from. Inversion of control is perhaps the most important design pattern ever devised, and IoC frameworks make or break any project.
Where exactly did you get your personal assertion from?
This always made OOP so frustrating to me. You have to go look in tons of places and build a mental model of all of them to have an idea what code does.
(But then you had to keep the diagramms in sync with the code)
OT: I remember someone noting that he tried to have modular code with as little functionality per module but reached a point where searching for things took writing it.
I sometimes ponder if it would be possible to create a DSL for every tiny niche part of each application. It would likewise depend on how awesome the documentation is and if you can even find it.
I think LLM's could make both ideas viable. One would be able to find anything and figure out how it works.
Could have a committee that judges how reusable your language extensions are and possibly grades you as a programmer with hints on how to improve.
Writing the coffee machine DSL should give you bragging rights.
A bit like languages nowadays accumulate new features.
In an ideal world one would write functions, the repetition of those would be documented and spawn language features, repetition and usage of those would spawn features in the lower language in which the language is implemented which in turn would spawn machine instructions. All using real data in stead of a cultivated hunch or popularity contest. How many installs, how often does that part of the code run, how many cycles can we save globally.
The objects in OO really don’t need to have anything to do with the outside world.
Projects where the majority of the code sits in a handful of giant bloated objects passing messages to each other tends to be quite flexible in practice. This works because rather than treating objects as consistent bundles of data and code throughout the program it’s often useful to think in terms of different contexts and then pass messages between those contexts. The advantage being you have fewer side effects to worry about when editing code, which is really the entire point of OO.
I'd take that a step farther - if they do, that's probably a sign that you're taking things too literally. They probably _shouldn't_ have anything to do with the outside world. I die a little inside every time somebody introduces inheritance with that stupid Dog extends Animal example.
I've seen developers claiming that "encapsulation" is just about "having getters/setters and private fields". But those getter/setters never get any custom implementation, they're just accessors for other classes.
It's not really OOP anymore at this point. Classes are just records, or depositories of procedural subroutines.
In the name of Single Responsibility Principle, there's a lot of breaking up classes in a way that forces encapsulation to be broken. A class doesn't always have to expose all its data!
The difference between FP/OOP and AOP is that with FP/OOP you know where to look. References outside the file you are currently looking at are usually declared at the top of the file. While with AOP there are no references in the files that are affected by AOP code.
With a more functional style you often rather make consumers to pass a function with behavior modifying code, the upside is that the specialization is given from the call-site and that the callee can is more explicit about passing out things without needing to care about the specialization context, and often it's easy to compose specializations in this way in one place.
OOP emphasizes encapsulation, decomposition, and dependency injection more aggressively than other paradigms. See SOLID. [1] While not necessarily bad, it does result in more "pieces."
Java -- arguably holding the #1 spot for OOP popularity -- requires that each (public) class be in its own file. This results in a lot of files.
AOP was pretty big in Enterprise Java shops in ~2007 era and I'm also glad that it never took off. It was one of those fads driven by "Architecture astronauts" and it resulted into lots of bloated code and delayed/failed projects.
I vaguely remember one major project from Visa that used AOP heavily and was an unmaintainable hot potato. It was passed on to different consulting companies and everyone messed it further by adding their idea of AOP to it.
It is notably extremely useful as a concept for instrumenting software, which is exactly what e.g APMs are doing. ASMs can go a step further and not just gather information but take action (sanitising arguments or return values, or even interrupting calls)
(disclaimer: I used to work at Sqreen, now work at Datadog)
Short of having first class support for AOP we're left implementing our own solutions to hook and instrument on various languages.
Some examples:
https://github.com/DataDog/datadog-instrumentation-gateway-r...
https://github.com/sqreen/go-agent/blob/master/doc/instrumen...
Let’s making coding even more complicated so you don’t know what the src output will be.
As a Lisp person, Kiczales should have know better than to go around splitting atoms.
You're really planting a bomb when you capture not only every currently available method that begins with "Get", but any future one someone may add.
Computer programs should ideally have the property, that any identifier occurring in the system can be replaced by one having a different name (other than that of some existing identifier), such that the program's behavior doesn't change. The only thing significant about identifiers is whether two identifiers are the same one or not. An identifier's name is just for debugging, and for linkage across subsystem boundaries that do not admit the passage of object identity.
Generally though I agree, architecting your software around AOP features is a terrible idea. Strike the O in any programming paradigm and you will be better off.
It lets you play games like "when the legacy code makes certain calls here and here, we do our thing, with our state, without disturbing anything else". If it doesn't work out, the AOP stuff can be easily backed out and disabled (or further massaged), rather than reverting some commit over 37 files of legacy code.
It's kind of like the software equivalent of patching a circuit board with wires. You have a bad circuit board, already manufactured in some quantity, and need to demo the hardware tomorrow. No time to spin another rev of the board.
But note the irony there! The motivation behind AOP is to have separation of concerns. For each new requirement heaped onto to the system, we can ideally just write one new module (using AOP), so that that requirement's implementation is concentrated in one place.
Oh, someone wants logging for all the database calls? No problem; don't touch the database calls: let's write an AOP module which invisibly adds logging to them from a far. Then when you're looking at the database calls, you can pretend that logging of database calls doesn't exist (or not even know). When you do know there is such a thing, you have it in one place. Yay!
The thing is, why is it the database calls that are at the bottom? Because they were written first? Something has to get written without AOP in order for AOP to have something to hook into. One problem in AOP is that we can't start with an empty program, and then go through a list of concerns and implement them with AOP. It's just a kind of parasite that needs a host.
In the time since AOP, a lot more API designs have sprouted support for closures. That’s “around” semantics and the degenerate cases of around are before and after, which covers most of the space. Async/await fills in some of the remaining gaps.
He made disparaging remarks about Lisp and said that CLOS should be pronounced "see loss". Remember, that's the author of the Art of the Metaobject Protocol, and of the Closette project (portable implementation of CLOS-like object system).
The logic seems to be that loose coupling is better than tight coupling, and what's looser than zero coupling? It's perfect!
The trouble is that zero coupling is not really possible in a working system. Instead what you've done is introduce 'hidden coupling', where one can't see the connections between components in the system from the code.
Hidden coupling makes understanding and maintaining a software system much harder than it would be otherwise.
The reason we avoid tight coupling is so that our software can remain “soft.” We can continue to mould and manipulate it even as out requirements or understandings change.
But software with hidden coupling might as well be the Internet. Sure, Google might crawl Wikipedia, and it provides some information as a result, and your software might rely on the result of some query. But then when your query breaks do you need to make a change to Wikipedia?
That’s probably a pretty bad analogy, but it’s the way I routinely felt when I was a junior working in a “hidden coupling” software system where even knowing what queries were being made was a challenge.
This is unarguably true.
However, it seems that the tooling often doesn't offer that support, or it isn't broadly known that it does. You are also marrying the tool as well as the language at that point, which might be deemed acceptable by some, but probably not all (I am not a fan of Lombok for similar reasons to why I would not be a fan of this).
To me, you are better off with simpler abstractions and practices than amping up the base requirements of your developers and tooling.
Sadly I lost the contents of the old wiki. I did pour a lot of energy into that :) But I still have a page on my old blog: https://www.aspyct.org/aspyct/ (funny URL though \o/)
It's never been a huge hit, but from time to time I would get emails from students who were investigating it for a university project. That was fun :)
Is was adding some interesting features (notably pointcuts), but ultimately it didn't bring enough added value compared to a simple decorator in python, and I ceased development.
It's still available on SourceForge, for those interested: https://sourceforge.net/projects/aspyct/files/aspyct/
EDIT: wow that was 15 years ago. I'm not getting younger :p
That said, even the manual says [2]:
> advice-add can be useful for altering the behavior of existing calls to an existing function without having to redefine the whole function. However, it can be a source of bugs, since existing callers to the function may assume the old behavior, and work incorrectly when the behavior is changed by advice. Advice can also cause confusion in debugging, if the person doing the debugging does not notice or remember that the function has been modified by advice.
> For these reasons, advice should be reserved for the cases where you cannot modify a function’s behavior in any other way.
[0] https://www.gnu.org/software/emacs/manual/html_node/elisp/Ad... [1] https://mbork.pl/2022-09-17_Safe_killing_with_Emacs [2] https://www.gnu.org/software/emacs/manual/html_node/elisp/Ad...
For some reason, we hired an "architect" from a large Bank that used Java. He used to make proclamations like "we should write tests", but never actually got into more detail than that. One of his preachings was about aspect oriented programming and how it was going to be future. He did a talk on it, and I remember someone asking "what else does it apply to apart from logging and Auth?"...no answer.
Not long after, I left. 2 years there, probably over 15 engineers, I don't think we built anything close to a functioning software product.
For a decade I thought about C++ as being "the language for people who are forced to think long and hard about every line of code."
And then I saw "normal" C++ code everyone else was writing...
I now sort of like it (C++), especially after recent language additions.
In the end, aspects were always used for the same transversal concerns, which any serious framework would provide explicit support for anyway.
Going beyond these to use aspects for application domain modeling is popping open an immense can of carnivorous worms on the dev team.
Worth thinking about if, like our app, it's crazy spaghetti and call graph is based on runtime state (which makes static analyzers less useful.)
Also architects doing their stuff: they for sure know best how to couple boxes in a PowerPoint presentation. ;)
It was kind of a nightmare (to put it kindly). This was an insurance app where updates in the frontend would automagically be pushed into the backend algorithm, which would automagically push changes to the frontend, which would automagically push changes to the backend...
The client also didn't pay for regression tests (2007 was a long time ago!) so basically every week it infinite loop locked in some new and undebuggable way.
1: http://onward-conferences.org/2009/files/steimannessay.pdf
I'd say it's okay to use in combination with OOP when you (and your team) manage to strictly keep the Domain/Business stuff in OOP world and the infrastructural stuff (logging) in AOP. Whenever you start mixing any of these together or have overlapping concerns, then things will quickly get very messy. While logging is fine with AOP e.g. with Access control things easily get mixed up over time.
When I look back the last 1-2 decades, the pattern that I see is that one principle always won: Simplicity.
I'd like to end up with a little war story:
In 2017 I happened to work at a startup (~60 engineers) that started with a gazillion of Microservices in dozens of different languages and ecosystems. Then there was this Clojure guy that didn't even do that much of missionary work. When people started reading each others codebases they realized the simplicity of Clojure and later started porting/rewriting their things to it. At the same time, everything Scala was frowned upon; I vividly remember trying to understand the request a supposedly simple CRUD service was making to another service and I couldn't understand the program flow for like 2h. Needless to say that probably like 80% of the services ended up being rewritten in Clojure and the Clojure guy got called the Johnny Appleseed of the company.
There were many other suggested use cases of course: transactions, caching, application-level security, dependency injection, retries, refactoring a legacy app...
The problem with all of the other suggested use cases is that AOP was never a great fit for them. AOP is always casting a wide net, which has the following two properties:
1. False positives: It's very easy to wrap all methods in the com.myapp.* package which fit the pattern "interact*()" with some transaction manager, or to cache their result. But a few years later a hapless developer adds another "interact" method which suddenly starts to misbehave. Logging has very little impact on behavior, but most of the other use cases (even caching and retries) can greatly impact behavior, so you'd usually want more control on them.
Of course, you could define a very narrow pointcut which doesn't risk a false positive — but in that case, why shouldn't you just use an annotation?
2. Large amount of join points: AOP works great to save you writing a lot of boilerplate code when you need to modify a large number of functions in a similar way. In modern programming languages you'd often use annotations (which postdate AOP, especially within Java), but even adding a single annotation on thousands of methods is tedious and error-prone. But again, out of all these use-cases, only logging really often needs to do the exact same thing for thousands of methods.
Caching, Retries and Dependency Injection require more in-site knowledge that is easier to achieve and more readable with annotations: which values you want to cache, what dependencies to inject and what would be their life time and source, which retry strategy should you use, etc.
All game developers know Aspect-Oriented Programming, just not by that name, because it's at the core of one of the most successful platforms in the industry.
Unity3d is a $14B company built in large part on Aspect-Oriented Programming and Aspect-Oriented Design.
Every time you implement a Unity Behavior in C#, you're doing Aspect-Oriented Programming in a text editor. Every time you drag a Behavior onto a Game Object in the Unity Editor, you're doing Aspect-Oriented Programming in a graphical programming environment.
"Transactions, security, and logging all exemplify cross-cutting concerns."
Logging, maybe. Transactions and security? Hard disagree.
And then in the example it checks for enough funds to make the withdrawal, and then makes the withdrawal, not as an atomic ation. I don't think that's just nit-picking on a simple example - how AOP interacts with atomic actions is fundamental to the use-case.
If you write a COME FROM 20 anywhere in the program, then when line 20 executes, control will invisibly pass to your COMEFROM statement.
Now that I spend a lot of time using Typescript, I find I rarely have the same issues that required those solutions. I believe that is due to the module system but I'm not entirely sure. I'm trying to think of the classic AOP problems, like structured log providers. Maybe it is the standardization of logging interfaces and the fact that I can trivially write adapters that makes this a non-issue for me these days?
I suppose, things like context managers [1] in React solve a similar problem space. That is probably the closest modern equivalent annoyance I can think of to the kind of problem that AOP was designed to solve.
To be honest, the tools I've had available were always able to solve the problem well enough. Every AOP solution I have seen is an order of magnitude more complicated than the problem it was trying to solve.
https://dojotoolkit.org/reference-guide/1.9/dojo/aspect.html
Now dojo is gone. But sometimes I still will port this feature into react/vue project.
Alas none of thr original greatness of javascript has been allowed. The new language features are all 100% special snowflake things, miles apart from the everything-is-an-object world we had. So these annotations/transforms are load-time only. They make a bunch of changes to your classes or whatever as the class loads then they are erased, invisible, totally unable to be seen unless explicitly coded.
Dripping disdain for this weak ass shitty path JS has started following. We keep making invisible unreflective shit & it's all super shit. Private fields can't be reflected on at all. ESM took over half a decade to get any kind of module metadata like import.meta and there is absolutely positively none of the module registry notions we were talking up as ESM was being crafted.
Everything is a huge downgrade for a runtime where you can look around & see what you have. To me, the neat spirit of AOP is when you not just attach some static annotations & compile it away, but when the runtime can keep growing new ways to be self aware. When you can keep compounding & meta-mutating. But the language design keeps shoving us down narrower more limited paths where the runtime actively tries to blind itself. There's such a huge massive narrative here on language design, & it should be in textbooks & screamed out loud, but it's invisible & unseen & the forces of conservative secure encapsulation & hiding are the only ones with any power or say & everyone else is losing. We have AOP about to happen but it's sad AOP, ignorant AOP, blind AOP.
Dynamic runtime AOP is just one of many capabilities - like DI/dependency Injection - where really what we are doing is creating a powerful metaplatform for resources, for object instances (and factories). Having management over objects is intensely interesting & empowering. Declarative code is just a cheap way to explicitly wire up resources/instances, with no management layer. I loved & hope we get back to more interesting & ambitious times when object repositories become powerful interesting object generating & manipulating entities.
If you look at a system like PostSharp or AspectJ, they do it via attributes which modify the way in which a method is invoked, or even if it is invoked by the system.
There are other ways to use those systems, by programmatically targeting parts of code to modify via reflection, but that is the simplest and most common.
The are some other considerations like when (compilation time, run time, ...) It's a really interesting code organization strategy and the only problem I've experienced with it is that (for example decorators) introduce a static layering (the order of the decorators) that works against the inter-aspect dynamics. For example, a call only needs to be logged if there is a necessity of authentication in a distributed context. In essence, you might not be able to have a static ordering that works for use cases. (security on top, or on the bottom ?)
The main aspectj work sort of folded into the spring framework which still uses some parts of that as part of the annotation processing in e.g. hibernate. That's the main reason, it bundles byte code manipulation libraries. So it can weave in some aspects. But most of the aspectj language level features never really became a mainstream thing.
Decorators, annotations, and similar language constructs emerged as a good enough thing that did most of what AOP was trying to accomplish. Python added those around 2003. Java gained annotations with Java 1.5 which was a few years after that. Other languages use macros or similar features to achieve the same thing. Mostly good enough though IMHO people over use them.
BTW. if you are into functional programming, this should feel really wrong. Basically you are deliberately adding side effects to your code by saying around or before/after a call to this, also do that. Of course the goto functional programming solution of monads has its own issues in terms of complexity, philosophical debates, and people having a hard time understanding them. The researchers in Utrecht where I studied in the mid nineties were into monads big time. My compiler course was basically about the compiler generator technology they were working on. Monads top to bottom. I liked aspectj a lot better after that. Easier to grasp for my primitive brain.
A decorator in Python (and I guess other languages) is a nothing but a function of a function. And you’re taking those side effects out of your function. What’s not to like?
Not against that btw. and I always appreciated how pragmatic python can be with a lot of things.
A Python decorator may be "just a function", but in order to do anything useful it generally has to do some kind of nasty hack - mutable state, global state, monkeypatching or the like - because the meat of what a decorator does usually lies in what happens when you call a decorated function, not in when you define it.
It was all the rage in game programming. I never used it myself. As all of the other comments have mentioned, it ends up being a kind of anti-pattern. Nobody wants logic strewn about your code base. It might as well be called side-effect based programming.
But then I think to something like the bracket function from Haskell: https://hackage.haskell.org/package/base-4.18.0.0/docs/Contr... That's 2 orders of magnitude simpler and doesn't need arbitrary function definitions so a pointcut can be defined against a block of code.
But hooks solve this cross cutting problem. It's like going from promise callback hells to async await.
My personal theory is it's something decorator like that involves modifying something like a struct which gets converted into game effects after all the modifiers run
This article just made me wonder about this again
> now as some of you know, item combos have to be hard coded at this point due to how nutty some of the tears are in rebirth. the most requested update you guys wanted in afterbirth was item combos, so the team has been hard at work the past few weeks mostly focusing on said combos.
It was the most awful paradigm to use, had to develop a test harness just to figure out the configuration and control flow for experiments.
For it's very niche use cases I'm sure it's interesting, for anything else it's just too difficult.
Fun to argue though.
I think you mean either abstruse or obtuse.
Wouldn't something too difficult to comprehend be an impossible maintenance burden? Why would these be too different things?
There is no programming paradigm, language, framework or tool that is going to fix this.
Seatbelts and backup cameras in cars don't solve car accidents, but refinements are still steps forward, because systemic problems are not all or nothing.
Things that make low level code easier to understand and manipulate make whole systems harder to understand and manipulate. More abstractions make larger systems easier to reason about in the whole, but harder to debug at a low level. Each programmer gets angry at a different place in this spectrum. There is no refinement that helps here because every step you take is further towards one person’s taste and away from someone else’s.
AOP/AspectJ won the space, beating out the more analytical program-slicing approaches which were incomprehensible to most. Pointcuts were more understandable than slices.
It's best not to reduce AOP to interception or weaving. While "crosscutting concerns" was the animating motivation, Kiczales regretted giving any toe-hold for mechanistic ideas of AOP and removed the notion of "crosscut" or "weaving" from the language and messaging (even though load-time binary weaving was the driving feature for adoption). There were (toy) implementations in C++ et al.
The core idea is that a program has well-understood places and times - where's and when's - that a language should capture as one when they have integrity while preserving language semantics. So the join point model targets programmer-understandable things (like method call or exception handling), not just anything (like loops), and preserves type-safety and access control. Similarly, type-extensions ("inter-type declarations") were binary-compatible with the original code. Unlike extensions in Swift or Objective-C, these extensions could add state to an object, which could be published or just used privately by the aspect (traits or mixin's anyone?). Aspect instances could be created staticly, one-per-instance of an object, and even once per control-flow/thread, esp. to tunnel information from high in the call stack down below (i.e., through library call-backs, if in thread).
Tooling support was always a strong concern. It started with its own debugger and IDE, but was really a perfect match for Eclipse (and Java's then-new support for debug context for any language in bytecode). The Eclipse compiler beat javac by focusing on incremental builds, making Eclipse the best place to code. The compiler has always had to be forked and extended to support AspectJ, which is something of a black art. But it did mean you could browse any pointcuts in a code base, leaving some to use it for its program-slicing abilities :)
Obliviousness (like private static) is the safe default, but AspectJ also supported aspect-aware code with public extensions. So there were a range of aspects, from benign observers, through essential but still oblivious security/audits, to public aspects forming feature layers in a product-family scheme.
Today, Swift has many AOP-like features, and Python context managers transformed the language (so yeah, loops are sensible join points). But AspectJ was tied to Java, and all the enterprise programming-container models were careful to keep programs dependent on the containers (read: oracle databases with in-memory VM's and IBM JVMs). There's no after advice for handler join points because Java bytecode doesn't have determinate points for that. Generics and method and field handles strained the language when there has been just a few heroic maintainers. There was lots of discussion for extending the notion of control flow across threads and machines (like tracing now), but no real support in bytecode (and not enough consistency in thread models or package loading conventions to make any implementation broadly available).
And there were hard problems: can you weave an already-woven class? After much analysis and discussion, the simple answer won: The original class was preserved in the woven class as an annotation (bloating the disk size, but not the size as loaded), and that was used instead of trying to unweave the woven bytecode.
AspectJ survived for a long time both through careful semantics and by making a lot of pragmatic engineering decisions, in both cases stretching to make it usable for ordinary programmers. But it was always a derivative of Java, and was embraced/extended (and monetized and contained) by Spring. (Adrian Colyer was handed the project as it moved to Eclipse, and was the CTO of Spring.)
https://www.eclipse.org/aspectj/doc/next/progguide/index.htm...