As a Java developer, you’re all too familiar with the Spring framework. Spring’s support for Inversion of Control (IoC) and aspect-oriented Programming (AOP) has long been part of our development routine, as if Java development were born that way. There is a tendency to ignore what is common. Everyone is proficient in IoC and AOP, but few are quite sure why they use IoC and AOP at all.
Technology was certainly created to solve a problem, and to understand why IoC and AOP are used, you need to understand what problems you run into without them.
IoC
Let’s now assume that we’re back in the pre-IOC era, developing with traditional servlets.
Disadvantages of traditional development model
The three-tier architecture is a classic development pattern, where we typically separate view control, business logic, and database operations into a single class so that each responsibility is clear and easy to reuse and maintain. The general code is as follows:
@WebServlet("/user")
public class UserServlet extends HttpServlet {
// The object used to execute business logic
private UserService userService = new UserServiceImpl();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
/ /... Omit other code
// Execute business logic
userService.doService();
/ /... Back to page view}}Copy the code
public class UserServiceImpl implements UserService{
// The object used to manipulate the database
private UserDao userDao = new UserDaoImpl();
@Override
public void doService(a) {
/ /... Omit business logic code
// Perform database operations
userDao.doUpdate();
/ /... Omit business logic code}}Copy the code
public class UserDaoImpl implements UserDao{
@Override
public void doUpdate(a) {
/ /... Omit JDBC code}}Copy the code
The code is divided into three layers as the upper layer relies on the lower abstraction:
This layered approach to code is common in the industry, with the idea of separation of responsibilities at its core. The lower the level is, the higher the reuse degree is. For example, a DAO object is often used by multiple Service objects, and a Service object is often used by multiple Controller objects:
Be organized and organized. These reused objects are like components that can be used by multiple parties.
Although the inverted triangle looks very nice, one of the big problems with our current code is that we only reuse logic, not resources.
When the upper layer calls the next layer, it must hold the object reference of the next layer, namely the member variable. Currently we instantiate an object for each member variable, as shown below:
Each link creates the same object, resulting in a huge waste of resources. Multiple controllers should reuse the same Service, and multiple services should reuse the same DAO. Now it’s a Controller creating duplicate services, which in turn create duplicate DAOs, from an inverted to a positive triangle.
Many components only need to instantiate one object; creating more than one makes no sense. The singleton pattern is a natural response to the problem of repeated object creation. Whenever you write a class as a singleton, you avoid wasting resources. However, the inherent complexity of introducing design patterns, and the fact that every class is a singleton and every class has similar code, is self-evident.
Some people might say, well, I don’t care that I’m wasting resources, I don’t care that I have a lot of memory on my server, I just want it to be easy and I don’t want to write extra code.
Indeed, the three-tier architecture is already very convenient for logical reuse, what else could you ask for? But despite the resource issues, the current code has a fatal flaw: it’s too expensive to change.
Let’s say I have 10 controllers that depend on UserService. I’m instantiating UserServiceImpl, and then I’m going to change the implementation class OtherUserServiceImpl. Very troublesome. The need to change implementation classes may not be too great to be persuasive. So let’s look at another situation.
The component creation process shown in the previous section is very simple. It takes a while to create a component, but many times creating a component is not that easy. For example, DAO objects depend on a data source component:
public class UserDaoImpl implements UserDao{
private MyDataSource dataSource;
public UserDaoImpl(a) {
// Construct the data source
dataSource = new MyDataSource("jdbc:mysql://localhost:3306/test"."root"."password");
// Do some other configuration
dataSource.setInitiaSize(10);
dataSource.setMaxActive(100);
/ /... More configuration items are omitted}}Copy the code
The data source component requires a lot of configuration to actually work, which can be cumbersome to create and configure. And the configuration may change frequently as business requirements change, so you need to modify every dependency on that component. This is just a demonstration of the creation and configuration of a data source, but there are so many components and configurations to code in real development that it can be horrendous.
Of course, all of these problems can be solved by introducing design patterns, but that brings us back to the point where design patterns themselves introduce complexity. It’s like an endless loop: traditional development models code complexity, but to solve one complexity you have to get stuck in another complexity. Is there no way to solve it? Of course not. Before we talk about good solutions, let’s take a look at the current problems:
-
Many duplicate objects are created, resulting in a large waste of resources.
-
Changing implementation classes requires multiple changes;
-
Creating and configuring components can be cumbersome and inconvenient for component callers.
Beneath the surface, these problems all occur for the same reason: the caller of the component is involved in creating and configuring the component.
What does it matter to the caller how the component is created and configured? For example, when I go to a restaurant, I only need to order the food. I don’t need to make the food myself. The restaurant will make it and bring it to me. If only there was a “thing” that helped us create and configure those components while we were coding, we could just call them. This “thing” is the container.
We’ve already touched on the concept of a container. Tomcat is a container for servlets. It helps us create and configure servlets, and we just write the business logic. Imagine how much code we would need if servlets were created and HttpRequest and HttpResponse objects were configured.
Tomcat is a Servlet container and only manages servlets. The components we normally use need to be managed by another container, which we call the IoC container.
Inversion of control and dependency injection
Inversion of control, in which control of object creation and configuration is transferred from the caller to the container. Like cooking at home, the taste of food is completely controlled by oneself; When you go to a restaurant, the taste of the food is controlled by the restaurant. The IoC container serves as the restaurant.
With the IoC container, we can commit objects to container-managed objects called beans. The caller is no longer responsible for creating the component, but simply obtains the Bean to use the component:
@Component
public class UserServiceImpl implements UserService{
@Autowired / / get a Bean
private UserDao userDao;
}
Copy the code
The caller simply declares the Dependency according to the convention, and the required Bean is automatically configured, as if a Dependency was injected outside the caller for use. This method is called Dependency Injection (DI). Inversion of control and dependency injection are two sides of the same coin, both manifestations of the same development pattern.
The IoC easily solves the problem we just summarized:
When objects are managed by the container, they default to singletons, which eliminates the resource waste problem.
To change the implementation class, simply change the declared configuration of the Bean to achieve no awareness replacement:
public class UserServiceImpl implements UserService{... }// Declare the implementation class as a Bean
@Component
public class OtherUserServiceImpl implements UserService{... }Copy the code
Component usage is now completely separate from component creation and configuration. The caller only needs to call the component and does not need to care about other work, which greatly improves our development efficiency and makes the whole application full of flexibility and expansibility.
In this regard, we are right to be so attached to the IoC.
AOP
Let’s imagine what would happen without AOP.
Limitations of object orientation
Object-oriented programming (OOP) three characteristics: encapsulation, inheritance, polymorphism, we have long been used to master. The benefits of OOP need not be overstated, and I believe that you all experience. Here’s a look at the limitations of OOP.
When duplicate code occurs, it can be encapsulated and reused. We plan the different logic and responsibilities by layering, subcontracting, and categorizing, just like the three-tier architecture explained earlier. However, the reuse here is only the core business logic, and cannot reuse some auxiliary logic, such as: logging, performance statistics, security verification, transaction management, etc. This edge logic often runs through your core business and is difficult for traditional OOP to encapsulate:
public class UserServiceImpl implements UserService {
@Override
public void doService(a) {
System.out.println("-- Safety check --");
System.out.println(-- Performance statistics Start--);
System.out.println(-- Log print Start--);
System.out.println("-- Transaction management Start--");
System.out.println("Business Logic");
System.out.println("-- Transaction management End--");
System.out.println("-- Log print End--");
System.out.println("-- Performance statistics End--); }}Copy the code
For demonstration purposes, only print statements are used here, but even so the code looks uncomfortable, and the logic is added to all business methods, which is scary to think about.
OOP is top-down programming, like A tree, where A calls B, B calls C, or A inherits B, AND B inherits C. This approach is appropriate for business logic to reuse through invocation or inheritance. The auxiliary logic is like a knife that cuts across all methods, as shown in Figure 2-4:
The horizontal lines cut through the OOP tree, like a big cake with multiple layers, and each layer performs the same auxiliary logic, which is why these auxiliary logic are called layers or cuts.
The proxy pattern is perfect for adding or enhancing existing functionality, but the difficulty with aspect logic is not that it does not modify the existing business, but that it applies to all businesses. Enhancing one business class requires creating a new proxy class, and enhancing all businesses requires creating a new proxy class for each class, which is a recipe for disaster. If I add a performance statistics section, I have to create a new section proxy class for logging printing. Once there are many sections, this proxy class will become very deeply nested.
Aspect-oriented programming (AOP for short) is a technology born to solve this problem.
Section-oriented programming
AOP is not the opposite of OOP; it is a complement to OOP. OOP is vertical, AOP is horizontal, the combination of the two can build a good program structure. AOP allows us to make aspect logic work in all business logic without modifying the original code.
We simply declare an aspect and write the aspect logic:
@Aspect // Declare an aspect
@Component
public class MyAspect {
// Before the original business method is executed
@Before("execution(public void com.rudecrab.test.service.*.doService())")
public void methodBefore(a) {
System.out.println("=== Before the AspectJ method is executed ===");
}
// After the original business method is executed
@AfterReturning("execution(* com.rudecrab.test.service.. doService(..) )"
public void methodAddAfterReturning(a) {
System.out.println("===AspectJ method after execution ==="); }}Copy the code
Whether you have one business method or ten thousand business methods, for us developers, writing aspect logic once makes all business methods effective, greatly increasing our development efficiency.
conclusion
The IoC addressed the following issues:
-
Many duplicate objects are created, resulting in a large waste of resources.
-
Changing implementation classes requires multiple changes;
-
Creating and configuring components can be cumbersome and inconvenient for component callers.
AOP solves the following problems:
- Aspect logic is cumbersome to write, as many times as there are business methods.
I’m RudeCrab, a RudeCrab, and I’ll see you in the next article.
Follow the “RudeCrab” wechat official account to bully with crabs.