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.
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.
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.
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:
Therefore, we can draw the following conclusions:
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.
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".
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;
...
}
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;
...
}
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());
}
...
}
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) {...}
...
}
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.
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.
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.
Therefore, writing business code into the controller class does not comply with the three-tier architecture specifications of the SpringMVC server.
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.
The following description takes the direct query of the database persistence middleware Hibernate as an example.
/** 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;
}
}
/** 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;
}
}
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:
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.
/** 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);
}
}
/** 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.
/** 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.
The following describes how to build a Java project more scientifically to effectively prevent developers from exposing the database model class to interfaces.
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:
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.
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).
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).
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).
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.
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);
}
}
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.
How Dubbo-Based Microservices Are Used in Insurance Companies
The Refactoring Principle and Why It Matters to Anyone Coding
206 posts | 12 followers
FollowAlibaba Clouder - November 21, 2019
Alibaba Cloud Native Community - April 22, 2021
JDP - February 18, 2022
Alibaba Cloud Native - October 12, 2024
Alibaba Clouder - November 23, 2020
Alibaba Clouder - March 26, 2020
206 posts | 12 followers
FollowA low-code development platform to make work easier
Learn MoreHelp enterprises build high-quality, stable mobile apps
Learn MoreAn on-demand database hosting service for SQL Server with automated monitoring, backup and disaster recovery capabilities
Learn MoreLeverage cloud-native database solutions dedicated for FinTech.
Learn MoreMore Posts by Alibaba Cloud Native
Dikky Ryan Pratama May 9, 2023 at 5:48 am
thank you for wanting to share