
状态机,就像现实中的交通信号灯控制器,绿灯(通行)→ 黄灯(过渡)→ 红灯(停止), 这种状态变化的规则就是状态机的典型应用。
Spring 状态机 是基于 Spring 框架的状态机实现,通过 spring-statemachine-core 组件(2.5.0 + 版本),能够帮助我们:
• 规范定义程序状态集合 :如订单的待支付、已支付、配送中、已完成等状态。
• 精准管理状态转换规则 :例如支付事件触发待支付 → 已支付的转换。
• 高效处理状态转换时的业务逻辑 :在状态变化过程中执行相应的操作。
当你的程序面临以下场景时,Spring 状态机将是一个绝佳的选择:
• 业务流程复杂 :涉及多个状态和事件交互,传统 if-else 实现方式难以维护。
• 状态转换规则频繁变化 :需要灵活调整状态转换逻辑,避免代码大量修改。
• 需要对状态进行集中管控 :以便统一监控和管理业务流程的状态变化。
传统 if-else 实现方式存在诸多痛点,例如:
if(currentState == "待支付"){ if(event == "支付"){ if(金额校验通过){ currentState = "已支付"; } } } else if(currentState == "已支付"){ }
这种代码结构随着业务复杂度增加,会变得难以维护和扩展。
在项目中引入 Spring 状态机的依赖,添加以下代码至 pom.xml 文件:
<dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-starter</artifactId> <version>3.2.0</version> </dependency>
注解 :
1.该依赖引入了 Spring 状态机的核心功能,为项目提供了状态机的实现和相关 API。
2.版本选择应根据项目实际需求和兼容性进行确定,3.2.0 版本具有良好的稳定性和功能支持。
接下来,定义状态和事件:
public enum OrderStates { UNPAID("待支付"), PAID("已支付"), DELIVERING("配送中"), COMPLETED("已完成"); private final String stateDesc; OrderStates(String stateDesc) { this.stateDesc = stateDesc; } public String getStateDesc() { return stateDesc; } } public enum OrderEvents { PAY("支付"), DELIVER("发货"), CONFIRM_RECEIVE("确认收货"); privatefinal String eventDesc; OrderEvents(String eventDesc) { this.eventDesc = eventDesc; } public String getEventDesc() { return eventDesc; } }
注解 :
1.OrderStates 枚举类定义了订单的四种状态,并为每个状态提供了描述信息,方便后续业务理解和展示。
2.OrderEvents 枚举类定义了订单的三种事件,同样添加了事件描述,使代码更具可读性。
然后,配置状态机:
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> { @Override public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states)throws Exception { states .withStates() .initial(OrderStates.UNPAID) // 设置初始状态为待支付 .states(EnumSet.allOf(OrderStates.class )); // 添加所有状态 } @Override public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions)throws Exception { transitions .withExternal() // 定义外部转换 .source(OrderStates.UNPAID) // 源状态:待支付 .target(OrderStates.PAID) // 目标状态:已支付 .event(OrderEvents.PAY) // 触发事件:支付 .and() .withExternal() .source(OrderStates.PAID) .target(OrderStates.DELIVERING) .event(OrderEvents.DELIVER) .and() .withExternal() .source(OrderStates.DELIVERING) .target(OrderStates.COMPLETED) .event(OrderEvents.CONFIRM_RECEIVE); } }
注解 :
1.EnumStateMachineConfigurerAdapter 是 Spring 状态机的配置适配器,用于简化状态机的配置过程。
2.在 configure 方法中,通过 withStates() 设置初始状态和所有状态,明确状态机的初始位置和可到达的状态范围。
3.在另一个 configure 方法中,使用 withExternal() 定义外部状态转换规则,依次设置源状态、目标状态和触发事件,清晰地展示了订单在不同状态之间的转换路径。
在业务逻辑中使用状态机:
public class OrderService { private StateMachine<OrderStates, OrderEvents> stateMachine; public OrderService(StateMachine<OrderStates, OrderEvents> stateMachine) { this.stateMachine = stateMachine; } public void handleEvent(OrderEvents event) { stateMachine.sendEvent(event); // 发送事件,触发状态转换 OrderStatescurrentState= stateMachine.getState().getId(); System.out.println("[状态跟踪] 当前订单状态:" + currentState.getStateDesc()); } }
注解 :
1.通过构造方法注入状态机实例,使其能够在业务逻辑中使用。
2.handleEvent 方法接收事件并发送给状态机,利用状态机的 sendEvent 方法触发状态转换。
3.获取当前状态并输出其描述信息,便于实时跟踪订单状态变化。
实现状态机监听器:
public class StateMachineListener extends StateMachineListenerAdapter<OrderStates, OrderEvents> { @Override public void transition(Transition<OrderStates, OrderEvents> transition) { System.out.printf("[状态机日志] 开始转换:%s -> %s (事件:%s)%n", transition.getSource().getId().getStateDesc(), transition.getTarget().getId().getStateDesc(), transition.getTrigger().getEvent().getEventDesc()); } @Override public void stateChanged(State<OrderStates, OrderEvents> from, State<OrderStates, OrderEvents> to) { System.out.println("[状态机日志] 转换完成,当前状态:" + to.getId().getStateDesc()); } }
注解 :
1.继承 StateMachineListenerAdapter 类,重写 transition 和 stateChanged 方法,实现对状态转换过程的监听。
2.在 transition 方法中,获取转换的源状态、目标状态和触发事件的描述信息,输出转换开始日志。
3.在 stateChanged 方法中,输出转换完成后的当前状态描述,方便对状态变化进行监控和记录。
Spring 状态机通过规范化的状态管理,能够有效提升复杂业务流程的可维护性。
它将业务流程中的状态和事件进行清晰的定义和管理,使得代码结构更加清晰、易于理解和维护。
以上内容对 Spring 状态机进行了简单介绍,希望对你有所帮助。
我曾踩过的代码坑记得刚接手一个电商项目时,看到支付模块的代码直接懵了——几百行的if-else嵌套,从信用卡支付到三方平台接口,每个条件分支都塞满了逻辑。
最崩溃的是,当产品说"要新增一个跨境支付方式"时,我盯着屏幕半小时不敢下手:改现有逻辑怕影响线上,新增分支又怕代码膨胀成面条。
后来在大佬的指点下用策略模式重构,才发现原来代码可以这么清爽。举个栗子:原本判断用户折扣的代码是这样的:
if (userType == "普通会员") { price = price * 0.9; } else if (userType == "黄金会员") { price = price * 0.8; // 后来又加了铂金会员、钻石会员... } else if (userType == "特邀会员") { price = price * 0.7 + 优惠券抵扣; }
每次新增会员等级都像在雷区跳舞,直到用策略模式把每个折扣规则拆成独立类,才真正体会到"开闭原则"的魅力——新增策略时只要写个新类,原来的代码一行都不用改。
用快递发货打个比方其实策略模式的核心思想特别接地气。
就像你寄快递时,不同场景会选不同物流:寄急件选顺丰,大件选德邦,便宜货选四通一达。
策略模式就是把这些"发货策略"封装起来,让系统能根据条件动态选择,而不用在代码里写死"如果是急件就选顺丰,否则选...".
在代码里,这个过程分为三步:
定义策略接口:比如ShippingStrategy接口里有个deliver()方法
实现具体策略:SFExpressStrategy、STExpressStrategy等类各自实现发货逻辑
上下文调度:ShippingService根据订单类型调用对应的策略在Spring Boot里集成更方便,靠依赖注入就能把策略塞进上下文里。
我常用的一个技巧是:在策略类上用@Component("sfExpress")这样的注解,然后在上下文里通过Map<String, ShippingStrategy>直接获取,键名就是注解里的字符串,比硬编码if-else爽太多。
从混乱到优雅的支付系统重构之前负责的一个跨境电商项目,支付模块支持信用卡、PayPal、本地钱包等七八种方式。
最初的代码是这样的:
if ("creditCard".equals(paymentType)) { // 信用卡支付逻辑,包括3D验证、风控检查... } else if ("paypal".equals(paymentType)) { // PayPal的授权流程,还要处理异步回调... } else if ("localWallet".equals(paymentType)) { // 本地钱包的特殊加密处理... }
每次新增支付方式都要改这个大条件块,更头疼的是单元测试——想测PayPal支付得把整个条件链跑一遍。
后来用策略模式重构:
1.定义支付策略接口
public interface PaymentStrategy { boolean process(PaymentRequest request); }
2.每个支付方式一个策略类
@Component("creditCard") public class CreditCardStrategy implements PaymentStrategy { @Override public boolean process(PaymentRequest request) { // 专注写信用卡支付逻辑,代码量减少60% } }
3.上下文类用Map管理策略
@Service publicclass PaymentService { privatefinal Map<String, PaymentStrategy> strategyMap; @Autowired public PaymentService(Map<String, PaymentStrategy> strategies) { this.strategyMap = strategies; } public boolean pay(PaymentType type, PaymentRequest request) { PaymentStrategy strategy = strategyMap.get(type.name().toLowerCase()); return strategy.process(request); } }
重构后最明显的变化:
新增支付方式时,我只需要写一个新策略类,然后在枚举里加个类型,原来的PaymentService一行都不用动。
测试也变得简单,每个策略类可以单独测,再也不用为了测一个分支跑通整个条件链了。
四、策略模式的隐藏技巧:
Lambda和Spring的神仙组合当策略逻辑比较简单时,甚至可以不用写一堆类。比如计算不同国家的税费,用Java 8的Lambda能把代码缩到极致:
// 在配置类里定义策略Map @Bean public Map<String, Function<Double, Double>> taxStrategyMap() { return Map.of( "CN", amount -> amount * 0.13, // 中国增值税 "US", amount -> amount * 0.07, // 美国销售税 "JP", amount -> amount * 0.10 // 日本消费税 ); } // 在服务里直接用 @Service publicclass TaxService { privatefinal Map<String, Function<Double, Double>> strategyMap; @Autowired public TaxService(Map<String, Function<Double, Double>> strategyMap) { this.strategyMap = strategyMap; } public double calculate(String country, double amount) { return strategyMap.get(country).apply(amount); } }
这种写法特别适合没有复杂状态的策略,比如计算规则、汇率转换等,既能享受策略模式的灵活性,又不用写一堆类,简直是懒癌患者的福音。
五、什么时候别用策略模式?
过来人的避坑指南虽然策略模式很香,但也不是万能药。我踩过的坑包括:
小场景别过度设计:如果只有两三种条件判断,比如"是管理员就显示全部按钮,否则显示部分",直接用if-else更直观
有状态的策略要小心:如果策略里需要维护全局状态(比如计数器),多个策略共享时可能出线程安全问题
Spring注入的坑:曾经忘记在策略类上加@Component,结果上下文获取时报空指针,debug半天才发现
还有个小技巧:当策略类很多时,可以用自定义注解来分组。比如我在项目里定义了@PaymentStrategy注解,然后在上下文里通过@Autowired注入List,再结合策略类上的@Order注解排序,比用Map更灵活。
六、从工具到思维:策略模式带来的架构思维升级
刚开始用策略模式时,只是觉得它能干掉讨厌的if-else。但随着项目经验增多,慢慢体会到它背后的设计哲学:把变化的部分封装成可替换的组件,让不变的逻辑保持稳定。
这种思维不仅适用于代码,也适用于架构设计——比如微服务里的路由策略、网关的限流策略,本质上都是策略模式的应用。
现在看到项目里出现超长的条件链,我第一反应就是:这里能不能用策略模式?当产品提出"以后可能会支持XX功能"时,也会下意识地用策略模式预留扩展点。这种思维方式的转变,可能比具体的代码技巧更有价值。
最后想说策略模式不是什么高深的黑魔法,而是从无数次代码维护痛苦中提炼出的解决方案。
它教会我们:好的代码不仅要能跑,还要能优雅地应对变化。下次当你面对满屏的if-else无从下手时,不妨试试策略模式——说不定会打开新世界的大门。
最烦的就是异常被层层包装,比如调用第三方接口时,IOException被包在ServiceException里,又被包在RuntimeException里,想找根因得一层层getCause()。
用 ExceptionUtil 的getCausedBy()一眼看穿:
try { // 调用可能抛出多层异常的方法 thirdPartyService.syncData(); } catch (Exception e) { // 直接找到最底层的IOException Throwable rootCause = ExceptionUtil.getCausedBy(e, IOException.class); if (rootCause != null) { log.error("数据同步失败,根本原因:{}", rootCause.getMessage()); // 处理IO异常(比如重试) } }
还有两个常用方法:
getMessage(e):获取完整异常信息(包括所有嵌套异常),日志里记这个准没错
stacktraceToString(e):把堆栈转成字符串,存数据库或日志文件特别方便
判断异常类型时,传统写法得堆一堆instanceof:
// 传统写法:又长又丑 catch (Exception e) { if (e instanceof IOException) { // 处理IO异常 } else if (e instanceof SQLException) { // 处理SQL异常 } else if (e instanceof TimeoutException) { // 处理超时异常 } }
用isCausedBy()一行搞定:
catch (Exception e) { if (ExceptionUtil.isCausedBy(e, IOException.class)) { log.error("IO异常:{}", e.getMessage()); } else if (ExceptionUtil.isCausedBy(e, SQLException.class)) { log.error("数据库异常:{}", e.getMessage()); } }
3. 受检异常转运行时:告别 try-catch 嵌套地狱
Java 里的受检异常(比如IOException)必须用 try-catch 处理,有时候会导致代码嵌套得像千层饼:
// 传统写法:受检异常逼你加try-catch public String readFile() { try { return Files.readString(Paths.get("config.properties")); } catch (IOException e) { throw new RuntimeException(e); // 手动包装成运行时异常 } } wrapRuntime()一键转换,连 Lambda 都支持: // 一行搞定,代码清爽多了 public String readFile() { return ExceptionUtil.wrapRuntime(() -> Files.readString(Paths.get("config.properties")) ); }
二、3 个实战场景,看完直接能用
`
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public Result handleException(Exception e) { // 日志里记录完整堆栈,方便排查 log.error("请求异常:{}", ExceptionUtil.stacktraceToString(e)); // 判断异常类型,返回对应提示 if (ExceptionUtil.isCausedBy(e, IllegalArgumentException.class)) { return Result.fail(400, "参数错误:" + e.getMessage()); } else { return Result.fail(500, "系统繁忙,请稍后再试"); } } }
2. 分布式任务:异常堆栈存数据库,方便追溯
定时任务或分布式任务失败时,用stacktraceToString()把堆栈存起来,后续排查问题有依据:
@Scheduled(cron = "0 0 3 * * ?") public void backupData() { try { // 执行数据备份 dataBackupService.backup(); } catch (Exception e) { // 堆栈转字符串存到日志表 String stackTrace = ExceptionUtil.stacktraceToString(e); taskLogService.saveLog("backupData", "FAILED", stackTrace); // 发送告警 alertService.send("数据备份失败:" + e.getMessage()); } }
3. 第三方接口调用:精准捕获特定异常,针对性处理
调用第三方接口时,经常会遇到各种异常,用getCausedBy()精准定位后针对性处理:
public void callPaymentApi(PaymentRequest request) { try { thirdPartyPayment.call(request); } catch (Exception e) { // 提取底层异常 Throwable root = ExceptionUtil.getCausedBy(e); if (root instanceof ConnectException) { // 网络连接异常,重试几次 retryCall(request); } elseif (root instanceof TimeoutException) { // 超时异常,记录到待处理队列 pendingQueue.add(request); } else { // 其他异常,直接告警 log.error("支付接口调用失败:{}", ExceptionUtil.getMessage(e)); } } }
三、为什么说它比原生异常处理强 10 倍?
代码量锐减:一行wrapRuntime()替代一堆 try-catch,Lambda 支持更友好
逻辑更清晰:isCausedBy()比一堆instanceof好懂多了,新人也能快速上手
性能优化:支持静态异常和堆栈清除,高并发场景下减少内存占用(比如频繁抛出的已知异常,可清除堆栈节省资源)
异常链透透透:不管异常被包了多少层,getCausedBy()总能找到根因,排查问题效率翻倍
四、最佳实践:这 3 招让异常处理更规范
日志记录要完整:用stacktraceToString()而不是e.getMessage(),避免关键信息丢失
自定义异常结合用:配合 Hutool 的StatefulException(带状态码的异常),实现业务异常分层:
// 自定义业务异常 public class OrderException extends StatefulException { public OrderException(int code, String message) { super(code, message); } } // 抛出时 if (order.getAmount() <= 0) { throw new OrderException(400, "订单金额必须大于0"); }
封装全局异常工具:把常用操作封装成工具类,全项目统一用法:
public class ExceptionHandlerUtil { public static void logAndAlert(Exception e, String taskName) { String stackTrace = ExceptionUtil.stacktraceToString(e); log.error("{}失败:{}", taskName, stackTrace); // 发送告警消息 messageService.sendToDingTalk("【" + taskName + "失败】" + e.getMessage()); } }
一、Spring Boot 核心注解:项目启动与配置
这是 Spring Boot 项目的核心注解,放在主启动类上,一眼就能认出这是个 Spring Boot 项目。它其实是 @Configuration、@EnableAutoConfiguration、@ComponentScan 三个注解的组合体。
@SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }
作用:开启自动配置、组件扫描和配置类功能,少了它 Spring Boot 就没法正常启动。
标记一个类是配置类,相当于传统的 XML 配置文件,里面可以用 @Bean 注解定义 bean。
@Configuration public class AppConfig { // 定义一个 RestTemplate 实例,全局可用 @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
优势:用 Java 代码写配置,比 XML 更灵活,还能加逻辑判断。
告诉 Spring 要扫描哪些包下的组件(@Controller、@Service 等),@SpringBootApplication 已经包含了它,默认扫描当前类所在的包及其子包。
// 手动指定扫描范围 @SpringBootApplication @ComponentScan(basePackages = {"com.example.controller", "com.example.service"}) public class MyApplication { // ... }
坑点:如果你的组件不在默认扫描范围内,就会注入失败,此时必须手动指定。
二、Web 开发注解:处理 HTTP 请求
标记一个类是 Spring MVC 的控制器,负责接收用户请求并返回视图(如 HTML 页面)。
@Controller public class UserController { // 处理 GET 请求,返回 user.html 页面 @GetMapping("/user") public String getUser(Model model) { model.addAttribute("name", "张三"); // 向页面传递数据 return"user"; // 返回 templates 目录下的 user.html } }
5. @RestController:API 接口的 “懒人注解”
这是 @Controller + @ResponseBody 的组合,返回的是 JSON 数据,不用再在每个方法上写 @ResponseBody 了。
@RestController public class ApiController { // 直接返回 JSON 数据 @GetMapping("/api/user") public User getUser() { User user = new User(); user.setName("李四"); user.setAge(25); return user; // 自动转为 JSON } }
适用场景:前后端分离项目的 API 接口,现在 90% 的 Spring Boot 项目都用它。
`
@RequestMapping("/user") @RestController public class UserController { // 处理 GET 请求:/user/1 @RequestMapping(value = "/{id}", method = RequestMethod.GET) public User getUserById(@PathVariable Long id) { // ... } }
简化注解:
@GetMapping:处理 GET 请求(常用)
@PostMapping:处理 POST 请求(常用)
@PutMapping:处理 PUT 请求
@DeleteMapping:处理 DELETE 请求
从 URL 路径中提取参数,比如从 /users/123 中获取 123 这个 id。
@GetMapping("/users/{id}") public User getUser(@PathVariable Long id) { return userService.findById(id); } // 多个参数 @GetMapping("/users/{id}/orders/{orderId}") public Order getOrder(@PathVariable Long id, @PathVariable Long orderId) { // ... }
8. @RequestParam:获取 URL 查询参数
获取 URL 中?后面的参数,比如从 /users?page=1&size=10 中获取 page 和 size。
@GetMapping("/users") public Page<User> getUsers( @RequestParam(defaultValue = "1") Integer page, // 默认值 1 @RequestParam(defaultValue = "10") Integer size) { return userService.findPage(page, size); }
常用属性:
required:是否必填(默认 true,不填会报错)
defaultValue:默认值(设置后 required 自动变为 false)
接收请求体中的 JSON 数据,并把它转换成对应的 Java 对象,POST、PUT 请求传递复杂对象时常用。
@PostMapping("/users") public User addUser(@RequestBody User user) { // user 已经是转换好的对象,直接保存即可 return userService.save(user); }
注意:前端必须设置 Content-Type: application/json,否则会报错。
三、依赖注入注解:管理组件关系
让 Spring 自动找到合适的 bean 并注入进来,不用自己 new 对象了。
@Service public class UserService { // 自动注入 UserDao @Autowired private UserDao userDao; public User getUserById(Long id) { return userDao.findById(id); } }
坑点:如果有多个同类型的 bean,直接用 @Autowired 会报错,此时要配合 @Qualifier 指定名称。
@Autowired @Qualifier("userDaoImpl") // 指定注入名称为 userDaoImpl 的 bean private UserDao userDao;
11. @Service:标记业务逻辑层
告诉 Spring 这是一个服务层组件,负责处理业务逻辑,会被自动扫描并注入。
@Service public class UserService { // 业务逻辑处理 }
同类注解:@Repository(数据访问层)、@Component(通用组件),它们都是为了让代码分层更清晰。
和 @Autowired 类似,但它默认按名称匹配,而 @Autowired 默认按类型匹配。
@Service public class OrderService { // 按名称注入(name 可省略,默认取属性名) @Resource(name = "orderDao") private OrderDao orderDao; }
小技巧:如果你更在意 bean 的名称,用 @Resource 更方便。
四、数据访问注解:操作数据库
加在方法或类上,让方法执行在事务中,出现异常时自动回滚,保证数据一致性。
@Service public class OrderService { // 该方法在事务中执行 @Transactional public void createOrder(Order order) { orderDao.save(order); // 保存订单 inventoryService.reduceStock(order.getProductId(), order.getNum()); // 扣减库存 // 如果上面的代码抛出异常,事务会回滚,订单和库存操作都取消 } }
常用属性:
rollbackFor:指定哪些异常会触发回滚(默认只有 RuntimeException 才回滚)
propagation:事务传播行为(如 REQUIRED、SUPPORTS 等)
标记一个接口是 MyBatis 的 Mapper 接口,不用写实现类就能直接调用。
@Mapper public interface UserMapper { // 直接写方法,SQL 在 XML 中或用注解写 User selectById(Long id); }
替代方案:在主启动类上用 @MapperScan 扫描整个包,不用每个接口都加 @Mapper。
@SpringBootApplication @MapperScan("com.example.mapper") // 扫描所有 Mapper 接口 public class MyApplication { // ... }
五、配置与参数绑定
直接读取 application.properties 或 application.yml 中的配置,注入到变量中。
# application.properties app.name=我的应用 app.version=1.0.0 @Component public class AppInfo { // 注入配置值 @Value("${app.name}") private String appName; @Value("${app.version}") private String appVersion; }
小技巧:可以用 {} 拼接字符串,比如 @Value("{app.name}-${app.version}")。
批量读取配置文件中的属性,绑定到一个类的字段上,比 @Value 更适合读取多个相关配置。
# 数据库配置 db.url=jdbc:mysql://localhost:3306/test db.username=root db.password=123456 @Component @ConfigurationProperties(prefix = "db") // 配置前缀 public class DbConfig { private String url; private String username; private String password; // getter 和 setter 必须有,否则无法绑定 }
优势:支持嵌套属性、校验(加 @Validated),还能在 IDE 中自动提示配置项。
六、其他高频注解
让方法返回的对象自动转为 JSON 数据,一般用在 @Controller 类的方法上。
@Controller public class ApiController { // 返回 JSON 数据 @GetMapping("/api/user") @ResponseBody public User getUser() { // ... } }
注意:@RestController 已经包含了它,所以不用重复加。
从 URL 路径中提取参数,比如从 /users/123 中获取 123 这个 id。
@GetMapping("/users/{id}") public User getUser(@PathVariable Long id) { return userService.findById(id); }
19. @RequestParam:获取查询参数
获取 URL 中?后面的参数,比如从 /users?page=1&size=10 中获取 page 和 size。
@GetMapping("/users") public Page<User> getUsers( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { return userService.findPage(page, size); }
20. @ExceptionHandler:全局异常处理
统一处理控制器中的异常,不用在每个方法中写 try-catch 了。
@RestControllerAdvice // 全局异常处理类 public class GlobalExceptionHandler { // 处理空指针异常 @ExceptionHandler(NullPointerException.class) public Result handleNullPointerException(NullPointerException e) { return Result.error("空指针异常:" + e.getMessage()); } // 处理所有异常(兜底) @ExceptionHandler(Exception.class) public Result handleException(Exception e) { return Result.error("系统异常,请联系管理员"); } }
效果:控制器抛出异常后,会自动被这里的方法捕获并处理,返回统一的错误格式。
安装
filebeat-<version>-windows 的文件夹。filebeat.yml 配置文件。可以使用文本编辑器(如 Notepad++ 或 Visual Studio Code)打开它。在 Windows 上,您可以将 Filebeat 作为服务运行。请按照以下步骤进行:
cd 命令导航到解压后的 Filebeat 文件夹。例如:
powershellcd "C:\Program Files\Filebeat\filebeat-<version>-windows"
powershell.\install-service-filebeat.ps1
powershellStart-Service filebeat
powershellGet-Service filebeat
powershellStart-Service filebeat
powershellGet-Service filebeat
C:\Program Files\Filebeat\logs 目录下。如果需要停止 Filebeat 服务,可以运行:
powershellStop-Service filebeat
如果需要卸载 Filebeat 服务,可以运行:
powershell.\uninstall-service-filebeat.bat
通过以上步骤,您应该能够在 Windows 上成功安装并运行 Filebeat。
filebeat.yml 示例yaml###################### Filebeat Configuration Example #########################
#=========================== Filebeat inputs =============================
filebeat.inputs:
# Apache 日志输入
- type: log
enabled: true
paths:
# 将以下路径改为对应的日志目录,可以使用 * 通配符匹配多个日志文件
- E:/RequirementsDocument/ELK-LOG/install/httpd-2.4.62-240904-win64-VS17/Apache24/logs/access*.log
fields:
log_type: apache
# IIS 日志输入
- type: log
enabled: true
paths:
- C:/inetpub/logs/LogFiles/W3SVC*/u_ex*.log
fields:
log_type: iis
# Tomcat 日志输入
- type: log
enabled: true
paths:
- D:/tomcat8.5.73/logs/catalina*.log
fields:
log_type: tomcat
# nginx 日志输入
- type: log
enabled: true
paths:
- E:/RequirementsDocument/ELK-LOG/logs/nginx/access*.log
fields:
log_type: nginx
#============================= Filebeat modules ===============================
filebeat.config.modules:
path: ${path.config}/modules.d/*.yml
reload.enabled: false
#==================== Elasticsearch template setting ==========================
setup.template.name: "its-log"
setup.template.pattern: "its-log-*"
setup.template.settings:
index.number_of_shards: 1
index.number_of_replicas: 0
_source.enabled: true
setup.template:
# Apache 模板
- name: "its-log-apache"
pattern: "its-log-apache*"
settings:
index.number_of_shards: 1
index.number_of_replicas: 0
_source.enabled: true
# IIS 模板
- name: "its-log-iis"
pattern: "its-log-iis*"
settings:
index.number_of_shards: 1
index.number_of_replicas: 0
_source.enabled: true
# Tomcat 模板
- name: "its-log-tomcat"
pattern: "its-log-tomcat*"
settings:
index.number_of_shards: 1
index.number_of_replicas: 0
_source.enabled: true
# nginx模板
- name: "its-log-nginx"
pattern: "its-log-nginx*"
settings:
index.number_of_shards: 1
index.number_of_replicas: 0
_source.enabled: true
#============================== Kibana =====================================
setup.kibana:
#================================ Outputs =====================================
#-------------------------- Elasticsearch output ------------------------------
output.elasticsearch:
# Array of hosts to connect to.
hosts: ["10.180.27.36:9200"]
index: "its-log-%{[fields.log_type]}-cd-%{+YYYY.MM.dd}"
username: "xxxx"
password: "xxxxxxxxx"
#================================ Procesors =====================================
processors:
- add_host_metadata: ~
- add_cloud_metadata: ~
#============================== Xpack Monitoring ===============================
number_of_files: 3
close_removed: true
clean_removed: true
#============================== Logging =====================================
logging:
level: info
to_files: true
files:
path: C:\Program Files\Filebeat\logs
name: filebeat
keepfiles: 7
permissions: 0644
###10 Windows filebeat.yml 示例解释
以下是对 Filebeat 配置文件的逐行解释:
yamlfilebeat.inputs:
# Apache 日志输入
- type: log
enabled: true
paths:
- E:/RequirementsDocument/ELK-LOG/install/httpd-2.4.62-240904-win64-VS17/Apache24/logs/access*.log
fields:
log_type: apache
filebeat.inputs:定义 Filebeat 的输入源。type: log:指定输入类型为日志文件。enabled: true:启用此输入。paths:定义要读取的日志文件路径,支持通配符(*)。fields:自定义字段,这里定义了 log_type 为 apache,可以用于后续的索引或处理。接下来的输入部分(IIS、Tomcat、nginx)与 Apache 输入类似,定义了不同类型的日志源,分别为:
C:/inetpub/logs/LogFiles/W3SVC*/u_ex*.logD:/tomcat8.5.73/logs/catalina*.logE:/RequirementsDocument/ELK-LOG/logs/nginx/access*.logyamlfilebeat.config.modules:
path: ${path.config}/modules.d/*.yml
reload.enabled: false
filebeat.config.modules:配置 Filebeat 模块。path:指定模块文件的位置。reload.enabled:是否启用模块的动态重载,这里设置为 false。yamlsetup.template.name: "its-log"
setup.template.pattern: "its-log-*"
setup.template.settings:
index.number_of_shards: 1
index.number_of_replicas: 0
_source.enabled: true
setup.template.name:设置索引模板的名称。setup.template.pattern:指定匹配的索引模式。setup.template.settings:定义索引的设置:
index.number_of_shards:将索引分为 1 个分片。index.number_of_replicas:不创建副本(0)。_source.enabled:启用源字段。接下来的部分定义了针对不同日志类型(Apache、IIS、Tomcat、nginx)的模板设置,基本上与上述设置相同,都是为不同类型的日志指定索引模板。
yamlsetup.kibana:
setup.kibana:用于配置 Kibana 的地址和设置。虽然没有具体配置,但这表示 Filebeat 将连接到 Kibana。yamloutput.elasticsearch:
hosts: ["10.180.27.36:9200"]
index: "its-log-%{[fields.log_type]}-cd-%{+YYYY.MM.dd}"
username: "xxxx"
password: "xxxxxxxx"
output.elasticsearch:配置 Filebeat 将数据发送到 Elasticsearch。hosts:指定 Elasticsearch 的主机和端口。index:定义索引名称的模式,使用 fields.log_type 来区分不同日志类型,并按日期格式化。username 和 password:用于连接 Elasticsearch 的凭据。yamlprocessors:
- add_host_metadata: ~
- add_cloud_metadata: ~
processors:用于在发送数据之前处理数据。add_host_metadata:添加主机元数据(如主机名、IP 地址等)。add_cloud_metadata:添加云元数据(如果适用)。yamlnumber_of_files: 3
close_removed: true
clean_removed: true
number_of_files 指定最大文件数,close_removed 和 clean_removed 控制日志文件的关闭和清理行为。yamllogging:
level: info
to_files: true
files:
path: C:\Program Files\Filebeat\logs
name: filebeat
keepfiles: 7
permissions: 0644
logging:配置 Filebeat 的日志设置。level:设置日志级别为 info。to_files:将日志输出到文件。files:定义日志文件的相关设置:
path:日志文件的存储路径。name:日志文件的名称。keepfiles:保留的日志文件数量。permissions:设置日志文件的权限。这个配置文件定义了 Filebeat 如何收集不同类型的日志(Apache、IIS、Tomcat、nginx),如何将这些日志发送到 Elasticsearch,并配置了相关的索引模板和日志记录设置。每个部分都可以根据实际需求进行调整,以确保 Filebeat 能够高效地收集和发送日志数据。
首先,确保您已经下载并解压了 Filebeat 的 tar.gz 包。如果还没有,可以使用以下命令下载并解压:
bash# 下载 Filebeat tar.gz 包(请根据需要替换版本号)
wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-6.8.23-linux-x86_64.tar.gz
# 解压缩
tar xzvf filebeat-6.8.23-linux-x86_64.tar.gz
# 进入解压后的目录
cd filebeat-6.8.23-linux-x86_64
在解压后的目录中,您会找到 filebeat.yml 配置文件。您需要根据您的需求编辑此文件(filebeat.yml示例请参考上面windows版本):
bashvim filebeat.yml
在安装 6.8.23 版本时,可能会出现权限问题,请在 filebeat.yml 文件中添加以下配置:
yamlseccomp:
default_action: allow
syscalls:
- action: allow
names:
- rseq
要在后台运行 Filebeat,您可以使用 nohup 命令或将其作为系统服务运行。以下是两种方法:
nohup 命令允许您在退出终端后继续运行程序。您可以使用以下命令在后台运行 Filebeat:
bashnohup ./filebeat -e -c filebeat.yml > filebeat.log 2>&1 &
-e:表示将日志输出到标准错误。-c filebeat.yml:指定配置文件。> filebeat.log 2>&1:将输出和错误重定向到 filebeat.log 文件。&:将命令放入后台运行。您可以使用以下命令查看 Filebeat 的日志输出:
bashtail -f filebeat.log
如果您希望更好地管理 Filebeat,建议将其作为系统服务运行。您可以创建一个系统服务单元文件,使 Filebeat 在系统启动时自动运行。
创建一个服务文件:
bashsudo nano /etc/systemd/system/filebeat.service
在文件中添加以下内容:
ini[Unit]
Description=Filebeat
Documentation=https://www.elastic.co/guide/en/beats/filebeat/current/index.html
After=network.target
[Service]
User=root
Group=root
ExecStart=/path/to/filebeat/filebeat -e -c /path/to/filebeat/filebeat.yml
Restart=always
[Install]
WantedBy=multi-user.target
请确保将 /path/to/filebeat/filebeat 和 /path/to/filebeat/filebeat.yml 替换为您实际的 Filebeat 可执行文件和配置文件的路径。
重新加载 systemd,使其识别新的服务:
bashsudo systemctl daemon-reload
启动 Filebeat 服务:
bashsudo systemctl start filebeat
设置 Filebeat 服务在系统启动时自动启动:
bashsudo systemctl enable filebeat
检查 Filebeat 服务状态:
bashsudo systemctl status filebeat
以上是在使用 tar.gz 包安装 Filebeat 后,在后台运行 Filebeat 的两种方法。使用系统服务的方式更为推荐,因为它提供了更好的管理和监控功能。
systemctl 启动的 Filebeat 服务的日志可以通过 journalctl 命令来查看。以下是查看 Filebeat 日志的步骤:您可以使用以下命令查看 Filebeat 的日志:
bashsudo journalctl -u filebeat.service -f
-u filebeat.service:指定要查看的服务。-f:实时跟踪日志输出,类似于 tail -f。如果您只想查看特定时间范围内的日志,可以使用 --since 和 --until 选项。例如,要查看今天的日志,可以这样做:
bashsudo journalctl -u filebeat.service --since "today"
如果您只想查看最近的几条日志,可以使用 -n 选项,例如查看最近的 50 条日志:
bashsudo journalctl -u filebeat.service -n 50
您还可以根据关键字过滤日志。例如,如果您想查找包含 "error" 的日志条目,可以使用:
bashsudo journalctl -u filebeat.service | grep "error"
如果您想查看所有日志而不限制于 Filebeat 服务,可以直接运行:
bashsudo journalctl -f
这将显示所有服务的实时日志输出。