Why Your Java Controllers and Services Are Misdesigned—and How to Fix Them
This article examines common misuse of base Controller and Service classes in Java micro‑service projects, critiques embedding business logic in Controllers, exposing persistence models to APIs, and offers concrete refactoring steps, project structuring patterns, and best‑practice guidelines to restore a clean three‑layer architecture.
1. Using Controller and Service Base Classes
1.1. Phenomenon Description
1.1.1. Controller Base Class
Typical Controller base class includes injected services, static constants, and static methods for all Controllers to inherit.
<code/** 基础控制器类 */
public class BaseController {
/** 注入服务相关 */
/** 用户服务 */
@Autowired
protected UserService userService;
/** 静态常量相关 */
/** 手机号模式 */
protected static final String PHONE_PATTERN = "/^[1]([3-9])[0-9]{9}$/";
/** 静态函数相关 */
/** 验证电话 */
protected static vaildPhone(String phone) { ... }
}</code>The base class mainly provides injection, constants, and utility methods for subclasses.
1.1.2. Service Base Class
Typical Service base class includes injected DAOs, services, parameters, constants, service methods, and static utility methods.
<code/** 基础服务类 */
public class BaseService {
/** 注入DAO相关 */
/** 用户DAO */
@Autowired
protected UserDAO userDAO;
/** 注入服务相关 */
/** 短信服务 */
@Autowired
protected SmsService smsService;
/** 注入参数相关 */
@Value("${example.systemName}")
protected String systemName;
/** 静态常量相关 */
/** 超级用户标识 */
protected static final long SUPPER_USER_ID = 0L;
/** 服务函数相关 */
protected UserDO getUser(Long userId) { ... }
/** 静态函数相关 */
protected static String getUserName(UserDO user) { ... }
}</code>The base class aggregates resources for all Service subclasses.
1.2. Argument for Necessity of Base Classes
According to the Liskov Substitution Principle, any place using a base class must be able to transparently use its subclasses. However, the presented base classes are never directly used, lack abstract methods, and merely provide convenience, which violates LSP and adds unnecessary loading overhead.
Subclasses inherit all methods and properties, reducing creation effort.
Improves code reuse.
Enhances extensibility.
Nevertheless, the current Controller and Service base classes do not satisfy LSP, have no abstract methods, and mix in resources that many subclasses do not need, leading to performance penalties.
1.3. How to Split Base Classes
1.3.1. Move Injection to Implementations
Inject only the required DAO, service, or parameter in the concrete class that needs it.
<code/** 用户服务类 */
@Service
public class UserService {
@Autowired
private UserDAO userDAO;
@Autowired
private SmsService smsService;
@Value("${example.systemName}")
private String systemName;
...
}</code>1.3.2. Extract Static Constants to Constant Classes
<code/** 例子常量类 */
public class ExampleConstants {
public static final long SUPPER_USER_ID = 0L;
...
}</code>1.3.3. Extract Service Functions to Service Classes
<code/** 用户服务类 */
@Service
public class UserService {
public UserDO getUser(Long userId) { ... }
...
}
/** 公司服务类 */
@Service
public class CompanyService {
@Autowired
private UserService userService;
public UserDO getManager(Long companyId) {
CompanyDO company = ...;
return userService.getUser(company.getManagerId());
}
...
}</code>1.3.4. Extract Static Functions to Utility Classes
<code/** 用户辅助类 */
public class UserHelper {
public static String getUserName(UserDO user) { ... }
...
}</code>2. Putting Business Code in Controllers
2.1. Phenomenon Description
Developers often place simple request handling logic directly in Controllers, arguing that a simple interface does not need a Service layer.
<code/** 用户控制器类 */
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserDAO userDAO;
@ResponseBody
@RequestMapping(path = "/getUser", method = RequestMethod.GET)
public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
UserDO userDO = userDAO.getUser(userId);
if (Objects.isNull(userDO)) {
return null;
}
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return Result.success(userVO);
}
...
}</code>The rationale is that the endpoint is simple, but this mixes presentation and business concerns.
2.2. A Special Case
<code/** 测试控制器类 */
@Controller
@RequestMapping("/test")
public class TestController {
@Value("${example.systemName}")
private String systemName;
@RequestMapping(path = "/access", method = RequestMethod.GET)
public String access() {
return String.format("系统(%s)欢迎您访问!", systemName);
}
}</code>The @Value injection fails because the WebApplicationContext does not inherit the parent context's BeanPostProcessor, leading to a null value.
2.3. Three‑Layer Architecture of Spring MVC
Spring MVC follows a classic three‑layer architecture: Presentation (Controller), Business (Service), Persistence (Repository).
Embedding business logic in Controllers violates this separation.
3. Putting Persistence Code in Services
3.1. Main Problems
Business and persistence layers are mixed, breaking the three‑layer rule.
Business logic becomes more complex due to inline query construction.
Direct use of third‑party middleware hinders replacement.
Persistence code scattered across business methods violates OOP principles.
Unit testing of persistence functions becomes difficult.
3.2. Example with Hibernate
<code/** 用户服务类 */
@Service
public class UserService {
@Autowired
private SessionFactory sessionFactory;
public UserVO getUserByEmpId(String empId) {
String hql = "from t_user where emp_id = '" + empId + "'";
Query query = sessionFactory.getCurrentSession().createQuery(hql);
List<UserDO> userList = query.list();
if (CollectionUtils.isEmpty(userList)) {
return null;
}
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userList.get(0), userVO);
return userVO;
}
}</code>Suggested Refactoring
<code/** 用户DAO类 */
@Repository
public class UserDAO {
@Autowired
private SessionFactory sessionFactory;
public UserDO getUserByEmpId(String empId) {
String hql = "from t_user where emp_id = '" + empId + "'";
Query query = sessionFactory.getCurrentSession().createQuery(hql);
List<UserDO> userList = query.list();
if (CollectionUtils.isEmpty(userList)) {
return null;
}
return userList.get(0);
}
}
/** 用户服务类 */
@Service
public class UserService {
@Autowired
private UserDAO userDAO;
public UserVO getUserByEmpId(String empId) {
UserDO userDO = userDAO.getUserByEmpId(empId);
if (Objects.isNull(userDO)) {
return null;
}
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
return userVO;
}
}</code>3.3. Redis Code in Service
<code/** 用户服务类 */
@Service
public class UserService {
@Autowired
private UserDAO userDAO;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String USER_KEY_PATTERN = "hash::user::%s";
public void saveUser(UserVO user) {
UserDO userDO = transUser(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);
userDAO.save(userDO);
}
}</code>Refactored Version
<code/** 用户Redis类 */
@Repository
public class UserRedis {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String KEY_PATTERN = "hash::user::%s";
public UserDO save(UserDO user) {
String key = MessageFormat.format(KEY_PATTERN, user.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);
return user;
}
}
/** 用户服务类 */
@Service
public class UserService {
@Autowired
private UserDAO userDAO;
@Autowired
private UserRedis userRedis;
public void saveUser(UserVO user) {
UserDO userDO = transUser(user);
userRedis.save(userDO);
userDAO.save(userDO);
}
}</code>4. Exposing Persistence Models to APIs
4.1. Phenomenon Description
<code/** 用户DAO类 */
@Repository
public class UserDAO {
public UserDO getUser(Long userId) { ... }
}
/** 用户服务类 */
@Service
public class UserService {
@Autowired
private UserDAO userDAO;
public UserDO getUser(Long userId) {
return userDAO.getUser(userId);
}
}
/** 用户控制器类 */
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@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);
}
}</code>This design directly returns the persistence entity UserDO to external callers, exposing database schema and potentially sensitive fields.
4.2. Problems and Solutions
Reveals internal table design to competitors.
Unrestricted fields waste bandwidth.
Sensitive data may be exposed.
Model‑to‑API mismatches cause maintenance issues.
Lack of clear documentation reduces maintainability.
Solutions:
Enforce strict separation of persistence models and API DTOs.
Structure projects to prevent Controllers from accessing DAOs directly.
4.3. Three Project Structuring Approaches
Approach 1: Shared Model Project
All model classes reside in a common module (example‑model) that other modules depend on, allowing the web layer to call service or repository code directly, which risks architectural violations.
Approach 2: Model‑Separated Project
Define an API module (example‑api) with DTOs and service interfaces. The service module implements these interfaces, and the web module only depends on the API module, reducing direct access to repositories.
Approach 3: Service‑Oriented Project
Package service and repository layers into a Dubbo service (example‑dubbo) that only publishes the API interfaces, ensuring the web layer can only invoke defined service contracts.
Afterword
Different developers have different opinions; the content presented reflects the author’s personal experience and suggestions for improving Java backend architecture in startup projects.
Author: Chen Changyi, technical expert at Gaode Map, Alibaba since 2018, focusing on map data collection.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
