Spring 项目别再乱注入 Service 了!用 Lambda 封装个统一调用组件,爽到飞起

兄弟们,咱做 Spring 项目的时候,是不是总遇到这些破事:

  • 每个 Controller 里都要写@Autowired UserService userService,注入一大堆 Service,代码又乱又冗余;
  • 想统一加个日志 / 异常处理,得在每个 Service 方法里写一遍,改起来要疯;
  • 偶尔还会手滑把 Service 类名 / 方法名写错,编译不报错,跑起来才出问题,排查半天。

今天给大家分享个我自己写的ServiceManager组件,用 Lambda 搞定这些破事 ------ 不用手动注入 Service,调用方法像写公式一样简单,还能自动缓存、统一处理异常,新手也能秒懂秒用!

先说说这组件能解决啥实际问题?

举个栗子:以前咱调用用户查询接口,得这么写:

kotlin 复制代码
// 1. 先注入Service
@Autowired
private UserService userService;
// 2. 再调用方法
public SerResult<UserDTO> getUser(Long userId) {
    try {
        log.info("开始查用户,ID:{}", userId);
        UserDTO user = userService.queryUser(userId);
        log.info("查询成功,结果:{}", user);
        return SerResult.success(user);
    } catch (Exception e) {
        log.error("查询失败", e);
        return SerResult.fail("查用户出错了");
    }
}

又是注入又是日志又是 try-catch,重复代码一堆。

用了ServiceManager之后,直接写成这样:

kotlin 复制代码
public SerResult<UserDTO> getUser(Long userId) {
    // 一行搞定:传方法+参数,其他全帮你做
    return ServiceManager.call(UserService::queryUser, userId);
}

注入?没了。日志?组件自动打。异常?组件自动处理。爽不爽?

组件核心逻辑:大白话拆解

其实这组件就干了 3 件事:

  1. 你传个 Lambda(比如UserService::queryUser),它帮你找到对应的 Service 实例;
  1. 把找到的实例和方法缓存起来,下次调用更快;
  1. 统一执行方法,顺便把日志、异常处理都包了。

下面咱一步步来,代码都给你贴好,复制过去改改就能用。

第一步:先搭基础 ------ 需要的依赖和工具类

首先得有几个小工具,不用自己写,直接复制:

1. 统一返回结果类(SerResult)

不管调用成功还是失败,都返回同一个格式,前端好处理:

typescript 复制代码
package org.pro.wwcx.ledger.common.dto;
import lombok.Data;
// 服务调用的统一返回结果,前端拿到就知道是成功还是失败
@Data
public class SerResult<T> {
    private int code;       // 200=成功,500=失败,前端一看就懂
    private String msg;     // 提示信息,比如"操作成功""查不到用户"
    private T data;         // 成功时返回的数据,比如用户信息
    // 成功的时候调用这个方法,把数据传进去
    public static <T> SerResult<T> success(T data) {
        SerResult<T> result = new SerResult<>();
        result.setCode(200);
        result.setMsg("操作成功");
        result.setData(data);
        return result;
    }
    // 失败的时候调用这个方法,传错误信息
    public static <T> SerResult<T> fail(String msg) {
        SerResult<T> result = new SerResult<>();
        result.setCode(500);
        result.setMsg(msg);
        result.setData(null);
        return result;
    }
}

2. Lambda 解析工具(LambdaUtil)

这工具是核心,帮咱从 Lambda 里 "扣" 出 Service 类名和方法名(不用懂原理,复制用就行):

java 复制代码
package org.pro.wwcx.ledger.common.util;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.invoke.SerializedLambda;
// 从Lambda表达式里拿Service信息的工具
public class LambdaUtil {
    // 传个Lambda进来,返回它对应的"元数据"(比如哪个Service,哪个方法)
    public static SerializedLambda valueOf(Serializable lambda) {
        if (lambda == null) {
            throw new IllegalArgumentException("Lambda不能传空!");
        }
        try {
            // 反射拿到Lambda里的隐藏方法,不用管这行是咋回事
            Method writeReplaceMethod = lambda.getClass().getDeclaredMethod("writeReplace");
            writeReplaceMethod.setAccessible(true);
            return (SerializedLambda) writeReplaceMethod.invoke(lambda);
        } catch (Exception e) {
            throw new RuntimeException("解析Lambda出错了", e);
        }
    }
}

3. Spring 工具类(SpringUtil)

帮咱从 Spring 里拿 Service 实例(不用手动@Autowired就是靠它):

java 复制代码
package org.pro.wwcx.ledger.common.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
// 从Spring里拿Bean的工具,不用自己注入Service
@Component
public class SpringUtil implements ApplicationContextAware {
    // Spring的上下文,相当于"Bean仓库"
    private static ApplicationContext applicationContext;
    // 从仓库里按类型拿Bean,比如拿UserService类型的实例
    public static <T> T getBean(Class<T> requiredType) {
        if (applicationContext == null) {
            throw new RuntimeException("Spring还没初始化好呢!");
        }
        return applicationContext.getBean(requiredType);
    }
    // 下面这行是Spring自动调用的,不用管
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtil.applicationContext = applicationContext;
    }
}

4. 函数接口(SerialBiFunction)

这个是 Lambda 的 "规矩",规定传参和返回值的格式(复制就行):

java 复制代码
package org.pro.wwcx.ledger.common.resolver.anno;
import java.io.Serializable;
// 支持序列化的双参数函数接口,Lambda要符合这个格式
public interface SerialBiFunction<T, U, R> extends Serializable {
    // 方法格式:传入T(Service实例)和U(参数),返回R(结果)
    R apply(T t, U u);
}

5. 实例构建器(InstBuilder)

帮咱快速创建对象的小工具,不用写一堆set方法:

csharp 复制代码
package org.pro.wwcx.ledger.common.resolver;
// 快速创建对象的工具,比如new ServiceExecutor后不用一个个set值
public class InstBuilder<T> {
    private final T target;
    // 初始化要创建的对象
    private InstBuilder(Class<T> clazz) {
        try {
            this.target = clazz.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException("创建对象失败", e);
        }
    }
    // 静态方法,入口:InstBuilder.of(ServiceExecutor.class)
    public static <T> InstBuilder<T> of(Class<T> clazz) {
        return new InstBuilder<>(clazz);
    }
    // 链式set值:比如.set(ServiceExecutor::setParam, param)
    public <V> InstBuilder<T> set(Setter<T, V> setter, V value) {
        setter.set(target, value);
        return this;
    }
    // 最后调用build()拿到对象
    public T build() {
        return target;
    }
    // 定义setter的格式
    @FunctionalInterface
    public interface Setter<T, V> {
        void set(T target, V value);
    }
}

第二步:核心组件 ------ServiceManager

这是咱的主角,所有逻辑都在这,我一行行给你讲明白:

swift 复制代码
package org.pro.wwcx.ledger.common.servicer;
import lombok.extern.slf4j.Slf4j;
import org.pro.wwcx.ledger.common.dto.SerResult;
import org.pro.wwcx.ledger.common.resolver.InstBuilder;
import org.pro.wwcx.ledger.common.resolver.anno.SerialBiFunction;
import org.pro.wwcx.ledger.common.util.LambdaUtil;
import org.pro.wwcx.ledger.common.util.SpringUtil;
import java.lang.invoke.SerializedLambda;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// 日志注解,能打日志
@Slf4j
public class ServiceManager {
    // 缓存初始化大小,6666够咱用了,不够再改
    private static final int INIT_COUNT = 6666;
    // 缓存Lambda对应的Service信息,key是Lambda,value是Service元数据
    private static final Map<SerialBiFunction<?,?,?>, LambdaMeta<?>> CACHE_LAMBDA;
    // 静态代码块,项目启动时就初始化缓存
    static {
        CACHE_LAMBDA = new ConcurrentHashMap<>(INIT_COUNT);
    }
    // 对外提供的调用方法:传Lambda(比如UserService::queryUser)和参数,返回结果
    @SuppressWarnings("unchecked")
    public static <T,U,R> SerResult<R> call(SerialBiFunction<T,U,R> fn, U param){
        // 先检查:Lambda不能传空
        if (fn == null) {
            return SerResult.fail("服务函数不能为空!");
        }
        // 1. 从缓存拿Service信息:有就直接用,没有就解析并缓存
        LambdaMeta<T> lambdaMeta = (LambdaMeta<T>) CACHE_LAMBDA.computeIfAbsent(fn, k-> {
            // 解析Lambda,拿到Service实例、类名这些信息
            LambdaMeta<T> meta = parseSerialFunction(fn);
            log.debug("缓存Service信息:{}", meta.getServiceName());
            return meta;
        });
        // 2. 创建执行器,把Lambda、参数、Service信息传进去
        ServiceExecutor<T,U,R> executor = InstBuilder.of(ServiceExecutor.class)
                .set(ServiceExecutor::setServiceFn, fn)    // 传Lambda方法
                .set(ServiceExecutor::setParam, param)      // 传参数
                .set(ServiceExecutor::setLambdaMeta, lambdaMeta)  // 传Service信息
                .build();  // 构建执行器
        // 3. 执行方法,返回结果
        return executor.callService();
    }
    // 解析Lambda:从Lambda里拿到Service类名、实例、方法名
    @SuppressWarnings("unchecked")
    private static <T, U, R> LambdaMeta<T> parseSerialFunction(SerialBiFunction<T,U,R> fn) {
        // 用LambdaUtil拿到Lambda的元数据
        SerializedLambda lambda = LambdaUtil.valueOf(fn);
        // 封装Service信息的对象
        LambdaMeta<T> lambdaMeta = new LambdaMeta<>();
        // 1. 解析Service类名:Lambda里的类名是"com/example/UserService",要改成"com.example.UserService"
        String tClassName = lambda.getImplClass().replaceAll("/", ".");
        try {
            // 2. 拿到Service的Class对象(比如UserService.class)
            Class<T> aClass = (Class<T>) Class.forName(tClassName);
            // 3. 从Spring里拿Service实例(不用@Autowired就是靠这行)
            T inst = SpringUtil.getBean(aClass);
            // 4. 把信息存到lambdaMeta里
            lambdaMeta.setClazz(aClass);    // 存Service的Class
            lambdaMeta.setInst(inst);       // 存Service实例
            lambdaMeta.setServiceName(lambda.getImplMethodName());  // 存方法名(比如queryUser)
        } catch (ClassNotFoundException e) {
            // 找不到类就抛异常
            throw new RuntimeException("没找到Service类:" + tClassName, e);
        }
        return lambdaMeta;
    }
    // 封装Service信息的内部类:存Class、实例、方法名
    @lombok.Data
    private static class LambdaMeta<T> {
        private Class<T> clazz;          // Service的Class(比如UserService.class)
        private T inst;                  // Service实例(Spring里的Bean)
        private String serviceName;      // 方法名(比如queryUser)
    }
}

第三步:执行器 ------ServiceExecutor

这是帮咱统一执行方法、打日志、处理异常的 "打工人":

java 复制代码
package org.pro.wwcx.ledger.common.servicer;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.pro.wwcx.ledger.common.dto.SerResult;
import org.pro.wwcx.ledger.common.resolver.anno.SerialBiFunction;
// 执行Service方法的类,统一打日志、处理异常
@Slf4j
@Setter
public class ServiceExecutor<T, U, R> {
    private SerialBiFunction<T, U, R> serviceFn;  // 要执行的Lambda方法
    private U param;                               // 方法参数
    private ServiceManager.LambdaMeta<T> lambdaMeta;  // Service信息
    // 执行方法的核心逻辑
    public SerResult<R> callService() {
        // 记录开始时间,方便算耗时
        long startTime = System.currentTimeMillis();
        String serviceName = lambdaMeta.getClazz().getSimpleName();  // 比如UserService
        String methodName = lambdaMeta.getServiceName();             // 比如queryUser
        log.info("开始调用:{}的{}方法,参数:{}", serviceName, methodName, param);
        try {
            // 真正执行方法:用Service实例调用Lambda方法
            R result = serviceFn.apply(lambdaMeta.getInst(), param);
            // 算耗时,打成功日志
            long costTime = System.currentTimeMillis() - startTime;
            log.info("调用成功:{}的{}方法,耗时{}ms,结果:{}",
                    serviceName, methodName, costTime, result);
            // 返回成功结果
            return SerResult.success(result);
        } catch (Exception e) {
            // 出错了就打错误日志,返回失败结果
            long costTime = System.currentTimeMillis() - startTime;
            log.error("调用失败:{}的{}方法,耗时{}ms",
                    serviceName, methodName, costTime, e);
            return SerResult.fail("调用" + serviceName + "的" + methodName + "方法失败:" + e.getMessage());
        }
    }
}

第四步:怎么用?举个实际例子

咱以用户查询和更新为例,看 Controller 里怎么写:

1. 先写个 Service(正常写,不用改)

kotlin 复制代码
package org.pro.wwcx.ledger.service;
import org.pro.wwcx.ledger.dto.UserDTO;
import org.pro.wwcx.ledger.dto.UserUpdateDTO;
import org.springframework.stereotype.Service;
// 正常的Service,该咋写咋写
@Service
public class UserService {
    // 查用户:根据ID查
    public UserDTO queryUser(Long userId) {
        // 这里模拟查数据库,实际项目里换JDBC/MyBatis
        UserDTO user = new UserDTO();
        user.setUserId(userId);
        user.setUserName("张三");
        user.setAge(25);
        return user;
    }
    // 更新用户:传ID和更新参数
    public Boolean updateUser(Long userId, UserUpdateDTO updateDTO) {
        // 这里模拟更新数据库
        log.info("更新用户{}的信息:{}", userId, updateDTO);
        return true;  // 返回更新成功
    }
}

2. Controller 里调用(重点看变化)

less 复制代码
package org.pro.wwcx.ledger.controller;
import org.pro.wwcx.ledger.common.dto.SerResult;
import org.pro.wwcx.ledger.common.servicer.ServiceManager;
import org.pro.wwcx.ledger.dto.UserDTO;
import org.pro.wwcx.ledger.dto.UserUpdateDTO;
import org.pro.wwcx.ledger.service.UserService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
    // 查用户:不用注入UserService!一行搞定
    @GetMapping("/{userId}")
    public SerResult<UserDTO> getUser(@PathVariable Long userId) {
        // 直接传Lambda(UserService::queryUser)和参数(userId)
        return ServiceManager.call(UserService::queryUser, userId);
    }
    // 更新用户:同样不用注入
    @PutMapping("/{userId}")
    public SerResult<Boolean> updateUser(
            @PathVariable Long userId,
            @RequestBody UserUpdateDTO updateDTO) {
        // 这里要注意:因为updateUser有两个参数,所以要显式指定Lambda类型
        return ServiceManager.call(
                (UserService service, UserUpdateDTO dto) -> service.updateUser(userId, dto),
                updateDTO
        );
    }
}

3. 跑起来看看效果

  • 查用户的时候,日志会自动打:
ini 复制代码
开始调用:UserService的queryUser方法,参数:1001
调用成功:UserService的queryUser方法,耗时5ms,结果:UserDTO(userId=1001, userName=张三, age=25)
  • 要是出错了,比如传个不存在的用户 ID(假设数据库查不到会抛异常),日志会打错误信息,返回给前端的结果是:
json 复制代码
{
  "code": 500,
  "msg": "调用UserService的queryUser方法失败:用户不存在",
  "data": null
}

这组件的好处:总结一下

  1. 不用再写 @Autowired:Controller 里干干净净,再也不用注入一堆 Service;
  1. 统一日志 / 异常:想改日志格式、加权限校验,只需要改 ServiceExecutor,不用改每个方法;
  1. 缓存优化:解析过的 Service 信息会缓存,下次调用更快;
  1. 类型安全:写 Lambda 的时候,方法名错了编译就报错,不用等到运行才发现。

注意事项:避坑指南

  1. JDK 版本:用 JDK8 及以上,Lambda 表达式是 JDK8 才有的;
  1. Service 要加 @Service:Spring 才能扫描到,不然 SpringUtil 拿不到实例;
  1. 多实现类的情况:如果一个接口有多个实现(比如 UserService 有 UserServiceImpl1 和 UserServiceImpl2),需要在 SpringUtil 里加按名称拿 Bean 的方法,具体可以评论区问我。

最后说明下,文档里的代码是我一步步设计出来的,文档润色部分内容借助豆包工具帮忙整理,方便大家阅读。完整的代码我已经整理好了,直接复制到项目里,改改包名就能用。如果遇到问题,或者想扩展功能(比如加缓存过期、多参数支持),评论区一起聊~

觉得有用的话,点个赞再走呗!

相关推荐
扣丁梦想家1 小时前
PostgreSQL 入门到精通 + Java & Spring Boot 实战教程
数据库·spring boot·postgresql
i***39582 小时前
Springboot中SLF4J详解
java·spring boot·后端
z***94842 小时前
springboot和springframework版本依赖关系
java·spring boot·后端
艺杯羹2 小时前
从Spring到SpringBoot3的演进:缺陷、优化与最新实践要求
java·spring boot·spring
一 乐2 小时前
宠物管理宠物医院管理|基于Java+vue的宠物医院管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·宠物
一 乐2 小时前
学习辅导系统|数学辅导小程序|基于java+小程序的数学辅导小程序设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·学习·小程序
励志成为糕手2 小时前
基于SpringBoot的企业考勤管理系统设计与实现
java·spring boot·后端·web·企业应用
w***4242 小时前
Springboot中使用Elasticsearch(部署+使用+讲解 最完整)
spring boot·elasticsearch·jenkins
e***74953 小时前
SpringBoot项目集成ONLYOFFICE
java·spring boot·后端