×
Community Blog Let's Talk about Some of the Chaos You'll Find on Java Servers in Startups

Let's Talk about Some of the Chaos You'll Find on Java Servers in Startups

This article looks at some of the chaos you'll find on Java servers in startup companies and looks at some ways to turn these serves around for the better.

By Chen Changyi, nicknamed Chang Yi, a technical expert on the AutoNavi team.

As Charles Dickens wrote in A Tale of Two Cities, "It was the best of times. It was the worst of times." With the rapid development of mobile Internet globally, many new opportunities have emerged, making it both the best and the worst of times for entrepreneurs and developers. Many entrepreneurs watch the market closely and keenly waiting for the right moment to act. But, as competition has gone more fierce in recent years, easy profits don't come as fast, good opportunities aren't so easy to come by, and many startup companies are struggling just make ends meet.

Before coming to Alibaba Cloud to work in the AutoNavi team, I worked in several different tech startups and have encountered several different Java microservice architectures. Thanks to this experience, I have learned many things. But beyond personal growth, I also discovered that the java servers at some startups are poorly configured, with there being some crazy configurations out there. In this article, I'm going to try to summarize some of the chaos you'll find on the Java servers at some startup companies, covering a variety of topics, and provide my suggestions on how to fix them.

The Controller Base Class and Service Base Class Are Used

Introduction to Base Classes

Controller Base Class

Listed below are some common controller base classes:

/** Controller Base Classes */
public class BaseController {    
    /** Injection services related */
    /** User Service */
    @Autowired
    protected UserService userService;
    ...

    /** Static constant correlation */
    /** Phone number mode */
    protected static final String PHONE_PATTERN = "/^[1]([3-9])[0-9]{9}$/";
    ...

    /** Static function related */
    /** Verify phone number */
    protected static vaildPhone(String phone) {...}
    ...
}

A common controller base class mainly contains injection services, static constants, and static functions, so that all controllers can inherit these resources from the controller base class and directly use these resources in functions.

Service Base Class

Common service base classes are listed as follows:

/** Service Base Classes */
public class BaseService {
    /** Injection DAO related */
    /** User DAO */
    @Autowired
    protected UserDAO userDAO;
    ...

    /** Injection services related */
    /** SMS service */
    @Autowired
    protected SmsService smsService;
    ...
    
    /** Injection parameters related */
    /** system name */
    @Value("${example.systemName}")
    protected String systemName;
    ...

    /** Injection constant related */
    /** super user ID */
    protected static final long SUPPER_USER_ID = 0L;
    ...

    /** Service function related */
    /** Get user function */
    protected UserDO getUser(Long userId) {...}
    ...

    /** Static function related */
    /** Get user name */
    protected static String getUserName(UserDO user) {...}
    ...
}

A common service base class mainly contains injection Data Access Objects (DAOs), injection services, injection parameters, static constants, service functions, and static functions, so that all services can inherit these resources from the service base class and directly use these resources in functions.

Necessity of the Base Class

First, let's take a look at the Liskov Substitution Principle (LSP).

According to the LSP, all the places that reference a base class (superclass) must be able to transparently use objects of its subclass.

Next, let's take a look at the advantages of the base classes:

  1. Base classes reduce the workload for creating a subclass because the subclass has all the methods and attributes of its superclass.
  2. Base classes improve the code reusability because a subclass has all the functions of its superclass.
  3. Base classes improve the code scalability because a subclass can add its own functions.

Therefore, we can draw the following conclusions:

  1. Controller base classes and service base classes are not directly used anywhere in the project, and they will not be replaced by any of their subclasses. Therefore, they do not comply with the LSP.
  2. Controller base classes and service base classes do not have abstract interface functions or virtual functions. That is, all subclasses that inherit a base class do not have common characteristics. As a result, what are used in a project are still subclasses.
  3. Controller base classes and service base classes only focus on the reusability. That is, a subclass can conveniently use resources of its base class, including injection DAOs, injection services, injection parameters, static constants, service functions, and static functions. However, controller base classes and service base classes ignore the necessity of these resources. That is, these resources are not essential for subclasses. Therefore, they cause the performance loss of subclasses when they are loaded.

To sum up, both controller base classes and service base classes can be categorized as miscellaneous classes. They are not base classes in the true sense and need to be split.

Methods for Splitting a Base Class

As service base classes are more typical than controller base classes, this article uses the service base classes as an example to explain how to split a "base class".

Put Injection Instances into the Implementation Class

According to the principle of "introducing the class only when the class is used and deleting it when it is not needed", inject the DAOs, services, and parameters to be used into the implementation class.

/** Udser Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** SMS service */
    @Autowired
    private SmsService smsService;

    /** System name */
    @Value("${example.systemName}")
    private String systemName;
    ...
}

Put Static Constants into the Constant Class

Encapsulate static constants into the corresponding constant class and directly use them when they are needed.

/** example constant class */
public class ExampleConstants {
    /** super user ID */
    public static final long SUPPER_USER_ID = 0L;
    ...
}

Put Service Functions into the Service Class

Encapsulate service functions into the corresponding service class. When using other service classes, you can inject this service class instance and call service functions through the instance.

/** User service class */
@Service
public class UserService {
    /** Ger user function */
    public UserDO getUser(Long userId) {...}
    ...
}

/** Company service class */
@Service
public class CompanyService {
    /** User service */
    @Autowired
    private UserService userService;
    
    /** Get the administrator */
    public UserDO getManager(Long companyId) {
        CompanyDO company = ...;
        return userService.getUser(company.getManagerId());
    }
    ...
}

Put Static Functions into the Tool Class

Encapsulate static functions into the corresponding tool class and directly use them when they are needed.

/** User Aid Class */
public class UserHelper {
    /** Get the user name */
    public static String getUserName(UserDO user) {...}
    ...
}

The Business Code Is Written in the Controller Class

Phenomena Description

We often see the code similar to the following in the controller class:

/** User Controller Class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function */
    @ResponseBody
    @RequestMapping(path = "/getUser", method = RequestMethod.GET)
    public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
        // Get user information
        UserDO userDO = userDAO.getUser(userId);
        if (Objects.isNull(userDO)) {
            return null;
        }
        
        // Copy and return the user
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userDO, userVO);
        return Result.success(userVO);
    }
    ...
}

Compilers may explain that it is fine that they write the code in this way because the interface function is simple, and it is unnecessary to encapsulate the interface function into a service function.

A Special Case

In this special case, the code is as follows:

/** Test Controller Class */
@Controller
@RequestMapping("/test")
public class TestController {
    /** System name */
    @Value("${example.systemName}")
    private String systemName;
    
    /** Access function */
    @RequestMapping(path = "/access", method = RequestMethod.GET)
    public String access() {
        return String.format("You're accessing System (%s)!", systemName);
    }
}

The access result is as follows:

curl http://localhost:8080/test/access

You're accessing System(null)!

You may ask why is the systemName parameter not injected? Well, the Spring Documentation provides the following explanation:

Note that actual processing of the @Value annotation is performed by a BeanPostProcessor.

BeanPostProcessor interfaces are scoped per-container. This is only relevant if you are using container hierarchies. If you define a BeanPostProcessor in one container, it will only do its work on the beans in that container. Beans that are defined in one container are not post-processed by a BeanPostProcessor in another container, even if both containers are part of the same hierarchy.

According to these explanations, @Value is processed through BeanPostProcessor, while WebApplicationContex and ApplicationContext are processed separately. Therefore, WebApplicationContex cannot use the attribute values of the parent container.

The controller does not meet the service requirements. Therefore, it is inappropriate to write the business code in the controller class.

Three-tier Server Architecture

A SpringMVC server uses the classic three-tier architecture, which is composed of the presentation layer, business layer, and persistence layer, which use @Controller, @Service, and @Repository for class annotation.

1

  • Presentation layer: It is also known as the controller layer. This layer is responsible for receiving requests from clients and responding to clients with results from the client. HTTP is often used at this layer.
  • Business layer: It is also known as the service layer. This layer is responsible for business-related logic processing and is divided by function into services and jobs.
  • Persistence layer: It is also known as the repository layer. This layer is responsible for data persistence and is used by the business layer to access the cache and database.

Therefore, writing business code into the controller class does not comply with the three-tier architecture specifications of the SpringMVC server.

The Persistence Layer Code Is Written in the Service Class

In terms of functionality, it is fine to write the persistence layer code in the service class. This is why many users are happy to accept this coding method.

Main Problems

  1. The business layer and persistence layer are mixed, which does not comply with the three-tier architecture specifications of the SpringMVC server.
  2. Statements and primary keys are assembled in the business logic, which increases the complexity of the business logic.
  3. Third-party middleware is directly used in the business logic, which makes it difficult to replace the third-party persistence middleware.
  4. The persistence layer code of the same object is scattered in various business logics, which is contrary to the principle of object-oriented programming.
  5. If this coding method is used to write the unit test cases, the persistence layer interface functions cannot be directly tested.

The Database Code Is Written in the Service

The following description takes the direct query of the database persistence middleware Hibernate as an example.

Symptom Description

/** User Service Class */
@Service
public class UserService {
    /** Session factory */
    @Autowired
    private SessionFactory sessionFactory;

    /** Get user function based on job number */
    public UserVO getUserByEmpId(String empId) {
        // Assemble HQL statement
        String hql = "from t_user where emp_id = '" + empId + "'";
        
        // Perform database query
        Query query = sessionFactory.getCurrentSession().createQuery(hql);
        List<UserDO> userList = query.list();
        if (CollectionUtils.isEmpty(userList)) {
            return null;
        }
        
        // Convert and return user
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userList.get(0), userVO);
        return userVO;
    }
}

Recommended Solution

/** User DAO CLass */
@Repository
public class UserDAO {
     /** Session factory */
    @Autowired
    private SessionFactory sessionFactory;
    
    /** Get user function based on job number */
    public UserDO getUserByEmpId(String empId) {
        // Assemble HQLstatement
        String hql = "from t_user where emp_id = '" + empId + "'";
        
        // Perform database query
        Query query = sessionFactory.getCurrentSession().createQuery(hql);
        List<UserDO> userList = query.list();
        if (CollectionUtils.isEmpty(userList)) {
            return null;
        }
        
        // Return user information
        return userList.get(0);
    }
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function based on job number */
    public UserVO getUserByEmpId(String empId) {
        // Query user based on job number
        UserDO userDO = userDAO.getUserByEmpId(empId);
        if (Objects.isNull(userDO)) {
            return null;
        }
        
        // Convert and return user
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userDO, userVO);
        return userVO;
    }
}

About the Plug-in

AliGenerator is a MyBatis Generator-based tool developed by Alibaba for automatically generating the DAO (Data Access Object) layer code. With the code generated by AliGenerator, you need to assemble query conditions in the business code when performing complex queries. This makes the business code especially bloated.

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function */
    public UserVO getUser(String companyId, String empId) {
        // Query database
        UserParam userParam = new UserParam();
        userParam.createCriteria().andCompanyIdEqualTo(companyId)
            .andEmpIdEqualTo(empId)
            .andStatusEqualTo(UserStatus.ENABLE.getValue());
        List<UserDO> userList = userDAO.selectByParam(userParam);
        if (CollectionUtils.isEmpty(userList)) {
            return null;
        }
        
        // Convert and return users
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userList.get(0), userVO);
        return userVO;
    }
}

I personally do not like using a plug-in to generate the DAO layer code. Instead, I prefer to use the original MyBatis XML for mapping because of the following reasons:

  • The plug-in may import some non-conforming code to the project.
  • To perform a simple query, you still need to import a complete set of complex code.
  • In a complex query, the code for assembling conditions is complex and not intuitive. It is a better choice to directly write SQL statements in XML.
  • After a table is changed, the code must be re-generated and overwritten, during which you may accidentally delete the user defined functions (UDFs).

If you choose to use the plug-in, you should also accept the disadvantages of the plug-in while enjoying the convenience brought by the plug-in.

The Redis Code Is Written in the Service Class

Symptom Description

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;
    /** Redistemplate */
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    /** User primary key mode */
    private static final String USER_KEY_PATTERN = "hash::user::%s";

    /** Save user function */
    public void saveUser(UserVO user) {
        // Convert user information
        UserDO userDO = transUser(user);

        // Save Redis user
        String userKey = MessageFormat.format(USER_KEY_PATTERN, userDO.getId());
        Map<String, String> fieldMap = new HashMap<>(8);
        fieldMap.put(UserDO.CONST_NAME, user.getName());
        fieldMap.put(UserDO.CONST_SEX, String.valueOf(user.getSex()));
        fieldMap.put(UserDO.CONST_AGE, String.valueOf(user.getAge()));
        redisTemplate.opsForHash().putAll(userKey, fieldMap);

        // Save database user
        userDAO.save(userDO);
    }
}

Recommended Solution

/** User Redis Class */
@Repository
public class UserRedis {
    /** Redistemplate */
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    /** Primary key mode */
    private static final String KEY_PATTERN = "hash::user::%s";
    
    /** Save user function */
    public UserDO save(UserDO user) {
        String key = MessageFormat.format(KEY_PATTERN, userDO.getId());
        Map<String, String> fieldMap = new HashMap<>(8);
        fieldMap.put(UserDO.CONST_NAME, user.getName());
        fieldMap.put(UserDO.CONST_SEX, String.valueOf(user.getSex()));
        fieldMap.put(UserDO.CONST_AGE, String.valueOf(user.getAge()));
        redisTemplate.opsForHash().putAll(key, fieldMap);
    }
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;
    /** User Redis */
    @Autowired
    private UserRedis userRedis;

    /** Save user function */
    public void saveUser(UserVO user) {
        // 转化用户信息
        UserDO userDO = transUser(user);

        // Save Redis user
        userRedis.save(userDO);

        // Save database user
        userDAO.save(userDO);
    }
}

Encapsulate a Redis object-related operation interface into a DAO class. This conforms to the object-oriented programming principle and three-tier architecture specifications of the SpringMVC server, and also facilitates code management and maintenance.

The Database Model Class Is Exposed to Interfaces

Symptom Description

/** User DAO Class */
@Repository
public class UserDAO {
    /** Get user function */
    public UserDO getUser(Long userId) {...}
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function */
    public UserDO getUser(Long userId) {
        return userDAO.getUser(userId);
    }
}

/** User Controller Class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User service */
    @Autowired
    private UserService userService;

    /** Get user function */
    @RequestMapping(path = "/getUser", method = RequestMethod.GET)
    public Result<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
        UserDO user = userService.getUser(userId);
        return Result.success(user);
    }
}

It seems that the preceding code conforms to the three-tier architecture of the SpringMVC server. The only problem is that the database model UserDO is directly exposed to external interfaces.

Existing Problems and Solutions

Existing Problems

  1. The database table design is indirectly exposed, which makes it convenient for competitors to analyze the competing products.
  2. If no field restrictions are imposed on database queries, the amount of interface data will be huge, wasting users' valuable traffic.
  3. If no field restrictions are imposed on database queries, sensitive fields are easily exposed to interfaces, resulting in data security issues.
  4. If the database model class cannot meet the interface requirements, you need to add other fields to the database model class, resulting in a mismatch between the database model class and the database fields.
  5. If the interface documentation is not properly maintained, reading code cannot help you identify which fields in the database model class are used by the interface. Therefore, the code maintainability becomes poor.

Solutions

  1. In terms of the management system, the database model class must be completely independent from the interface model class.
  2. In terms of the project structure, developers must be discouraged from exposing the database model class to interfaces.

Three Methods for Project Building

The following describes how to build a Java project more scientifically to effectively prevent developers from exposing the database model class to interfaces.

Method 1: Building a Project with a Shared Model

Put all model classes in one model project (example-model). Other projects, including example-repository, example-service, and example-website, all depend on example-model. The relationship diagram is as follows:

2

3

Risks

The presentation layer project (example-webapp) can call any service function of the business layer project (example-service), or even directly call the DAO functions of the persistence layer project (example-repository) across the business layer.

Method 2: Building a Project with a Separated Model

Build an API project (example-api) separately, and abstract the external interfaces and their model VO classes. The business layer project (example-service) implements these interfaces and provides services for the presentation layer project (example-webapp). The presentation layer project (example-webapp) only calls the service interfaces defined by the API project (example-api).

4

5_1

Risks

The presentation layer project (example-webapp) can still call the internal service functions of the business layer project (example-service) and DAO functions of the persistence layer project (example-repository). In order to avoid this situation, the management system must require that the presentation layer project (example-webapp) can only call the service interface functions defined by the API project (example-api).

Method 3: Building a Service-Oriented Project

Package the business layer project (example-service) and persistence layer project (example-repository) into a service by using the Dubbo project (example-dubbo). Provide the interface functions defined in the API project (example-api) for the business layer project (example-webapp) or other business project (other-service).

6

7_1

Note: The Dubbo project (example-dubbo) only releases the service interfaces defined in the API project (example-api). This ensures that the database model is not exposed. The business layer project (example-webapp) or other business project (other-service) only relies on the API project (example-api) and can only call the service interfaces defined in the API project.

A Less Recommended Suggestion

Some users may have the following considerations: Given that the interface model is separated from the persistence layer model, so that means that, if a VO class of the data query model is defined for the interface model, a DO class of the data query model should also be defined for the persistence layer model. And also, if a VO class of the data return model is defined for the interface model, a DO class of the data return model should also be defined for the persistence layer model. However, this does isn't very appropriate for rapid iterative development in the early stage of the project. Further, this also raises the following question: Is it possible to let the persistence layer use the data model of an interface without exposing the data model of the persistence layer through the interface?

This method is unacceptable for the three-tier architecture of the SpringMVC server because it affects the independence of the three-tier architecture. However, this method is acceptable for rapid iterative development because it does not expose the database model class. Therefore, it is a less recommended suggestion.

/** User DAO Class */
@Repository
public class UserDAO {
    /** Calculate user function */
    public Long countByParameter(QueryUserParameterVO parameter) {...}
    /** Query user function */
    public List<UserVO> queryByParameter(QueryUserParameterVO parameter) {...}
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Query user function */
    public PageData<UserVO> queryUser(QueryUserParameterVO parameter) {
        Long totalCount = userDAO.countByParameter(parameter);
        List<UserVO> userList = null;
        if (Objects.nonNull(totalCount) && totalCount.compareTo(0L) > 0) {
            userList = userDAO.queryByParameter(parameter);
        }
        return new PageData<>(totalCount, userList);
    }
}

/** User Controller Class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User service */
    @Autowired
    private UserService userService;

    /** Query user function (with the page index parameters of startIndex and pageSize) */
    @RequestMapping(path = "/queryUser", method = RequestMethod.POST)
    public Result<PageData<UserVO>> queryUser(@Valid @RequestBody QueryUserParameterVO parameter) {
        PageData<UserVO> pageData = userService.queryUser(parameter);
        return Result.success(pageData);
    }
}

Conclusion

Everyone has his or her own opinions about how one should make use of Java, and of course this article provides only my personal opinions. But, for me, I thought it was important to express my thoughts based on my experience in some of the startup companies that I have worked for previously. Because my understanding is that, if these chaotic configurations were fixed, then the entire system would be whole lot better.

1 1 1
Share on

Alibaba Cloud Native

206 posts | 12 followers

You may also like

Comments

Dikky Ryan Pratama May 9, 2023 at 5:48 am

thank you for wanting to share