03-Spring Boot

60 阅读10分钟

一、IOC、DI

IOC与DI是spring框架最重要的概念,IOC指把对象的创建交给spring容器,DI指如何使用Spring创建的对象。

1.IOC

在类顶部添加spring内置的注解即可,常见注解包括@Service,@RestController,@Component

@RestController
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {  // Spring 自动传入
        this.userService = userService;
    }
}

2.DI

DI指如何将Spring管理的类注入到当前类中。如下案例演示UserController类如何使用UserService类


@Service // ① UserService 注册为 Bean
public class UserService {
    private final UserMapper userMapper;
    public UserService(UserMapper userMapper) {  // ② 构造器注入
        this.userMapper = userMapper;
    }
}

@RestController  // ① UserController 注册为 Bean
public class UserController {
    private final UserService userService;
    public UserController(UserService userService) {  // ② 构造器注入
        this.userService = userService;
    }
}

3.第三方工具注入

spring中service、controller、mapper层的类通过注解实现spring的bean管理,但是第三方工具类被bean管理需要特殊操作

  1. 在xxxCondig配置类顶部添加注解@Configuration
  2. 任意定义方法,方法的返回值就是工具类,并且添加注解@Bean。此时这个类就被Bean管理了
@Configuration
public class AppConfig {

    @Bean
    public Clock clock() {
        return new Clock()
    }
}
  1. 其它类使用工具类通过构造器注入
@Service
public class TimeService {

    private final Clock clock;

    public TimeService(Clock clock) {
        this.clock = clock;
    }

    public String now() {
        return clock.instant().toString();
    }
}

二、web处理

spring的web处理包含,路由、参数都在control层。spring内置大量注解来直接解析http请求

@RequestMapping:路由

@GetMapping:get请求

@PostMapping:post请求

@RequestBody:请求体

@PathVariable:路径变量

@RequestParam:查询参数

@RestController
@RequestMapping("/api/users")
public class UserApiController {

    private final Map<Long, User> store = new HashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    // 构造时放两条测试数据
    public UserApiController() {
        store.put(1L, new User(1L, "张三", "zhangsan@example.com"));
        store.put(2L, new User(2L, "李四", "lisi@example.com"));
        idGenerator.set(3);
    }

    /** GET /api/users */
    @GetMapping
    public List<User> list() {
        return new ArrayList<>(store.values());
    }

    /** GET /api/users/1 */
    @GetMapping("/{id}")
    public User getById(@PathVariable Long id) {
        User user = store.get(id);
        if (user == null) {
            throw new RuntimeException("用户不存在: " + id);
        }
        return user;
    }

    /** POST /api/users  Body: {"username":"王五","email":"..."} */
    @PostMapping
    public User create(@RequestBody User user) {
        long id = idGenerator.getAndIncrement();
        user.setId(id);
        store.put(id, user);
        return user;
    }

    /** PUT /api/users/1  Body: {"username":"...","email":"..."} */
    @PutMapping("/{id}")
    public User update(@PathVariable Long id, @RequestBody User user) {
        if (!store.containsKey(id)) {
            throw new RuntimeException("用户不存在: " + id);
        }
        user.setId(id);
        store.put(id, user);
        return user;
    }

    /** DELETE /api/users/1 */
    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        store.remove(id);
    }
    
    
    // 查询参数  /users/search?keyword=张&page=1
    @GetMapping("/users/search")
    public List<User> search(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "1") int page) { }
}

三、环境配置

Spring Boot 使用 application.propertiesapplication.yml 统一管理配置。

server.port=8080
spring.application.name=spring_demo_maven

spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_demo
spring.datasource.username=root
spring.datasource.password=你的密码

3.1 读取配置

@value:在属性上添加@value即可读取配置项,缺点是需要逐个读取。

@RestController
public class ConfigDemoController {

    @Value("${spring.application.name}")
    private String appName;

    @Value("${server.port}")
    private int port;

    @GetMapping("/config/demo")
    public Map<String, Object> demo() {
        return Map.of("appName", appName, "port", port);
    }
}

@ConfigurationProperties

通过在类上添加注解@ConfigurationProperties,告诉spring这个类上的属性映射配置项内容。prefix前缀可以替代配置项的公共前缀。

//配置项内容
app.name=dzp
app.age=22
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String name = "默认标题";
    private int age = 1
    public void setName(String name) {
        this.name = name
    }
    public void setAge(int age) {
        this.age = age
    }
}

四、三层架构

spring标准的三层架构写法依次是

Mapper:数据库访问

Service:业务逻辑

Controller:接收请求,响应。

如下是一份案例,三层逐层调用。

/*Mapper层*/
@Repository
public class UserStore {
  private final Map<Long, User> store = new HashMap<>();
  private final AtomicLong idGenerator = new AtomicLong(1);
  public UserStore() {
    store.put(1L, new User(1L, "张三", "zhangsan@example.com"));
    idGenerator.set(3);
  }
  
  public User findById(Long id) {
    return store.get(id);
  }
  public User save(User user) {
    long id = idGenerator.getAndIncrement();
    user.setId(id);
    store.put(id, user);
    return user;
  }
  public boolean deleteById(Long id) {
    return store.remove(id) != null ? true : false;
  }
}

/*Service层*/
@Service
public class UserService{
    private final UserStore userStore;
    public UserService(UserStore userStore) {
        this.UserStore = userStore;
    }
    public User findById(Long id) {
      //业务校验id
      //调用mapper层数据库
      return userStore.findById(id);
    }
    
}

/*Controller层*/
@RestController
@RequestMapping("/api/users")
public class UserController {
  private final UserService userService;
  public UserController(UserService userService) {
    this.userService = userService;
  }
  
  @GetMapping("/{id}")
  public User getById(@PathVariable Long id) {
    return userService.findById(id)
  } 


五、异常

spring支持配置全局异常拦截,任何地方的异常都可以被全局异常拦截。

  1. 添加全局异常类,头部添加@RestControllerAdvice
  2. 添加多个异常类方法,头部添加注解@ExceptionHandler(自定义异常类.class)
  3. service、controller中抛出异常时,全局异常拦截器根据异常类型匹配方法返回包装的结果

@RestControllerAdvice
public class GlobalException {

  @ExceptionHandler(BusinessException.class)
  @ResponseStatus(HttpStatus.OK)
  public Result<Void> handleBusinessException(BusinessException e) {
    return Result.fail(e.getCode(), e.getMessage());
  }

  @ExceptionHandler(MethodArgumentNotValidException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
    FieldError fieldError = e.getBindingResult().getFieldError();
    String message = fieldError != null ? fieldError.getDefaultMessage() : "参数校验失败";
    return Result.fail(400, message);
  }

  @ExceptionHandler(IllegalArgumentException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public Result<Void> handleIllegalArgumentException(IllegalArgumentException e) {
    return Result.fail(400, e.getMessage());
  }

  @ExceptionHandler(Exception.class)
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  public Result<Void> handleException(Exception e) {
    return Result.fail(500, "服务器内部错误");
  }
}

如下代码在查询用户时,查询失败,抛出BusinessException异常,全局异常类根据类型定位到handleBusinessException这个方法,然后抛出result结果渲染到前端,而不是错误堆栈信息。

public User getById(Long id) {
    User user = userMapper.findById(id);
    System.out.println("user"+user);
    if (user == null) {
      throw new BusinessException(404, "用户不存在: " + id);
    }
    return user;
}

六、MyBatis-Plus

七、Redis

redis是非关系型数据库,用于进行数据缓存操作。常搭配mysql进行数据链路的缓存查询

1.id查询用户信息
2.优先查询redis,命中直接返回信息,否则查询mysql
3.查询mysql,缓存数据到redis

7.1 数据类型

redis只有五种数据类型string,hash,list,set,zset。

1.String

string保存字符串与数字数据,其api如下

  1. set key value:存储数据
  2. get key:获取数据
  3. exists key:是否存在数据,0不存在,1存在
SET name "张三"
GET name                  # "张三"
SET counter 0
INCR counter              # 1(计数器 +1)
DEL name                  # 删除 key

2.Hash

hash保存对象数据,其api如下

  1. hset name key1 value1 key2 value2 ...
  2. hget name:获取完整对象数据
  3. hget name key1:获取对象的属性是key1的
  4. hexists key:是否存在数据,0不存在,1存在
HSET profile:1 username "张三" email "a@b.com" phone "13800138000"
HGET profile:1 username          # 只取一个 field → "张三"
HGETALL profile:1                # 取全部 field+value(交替返回)
HLEN profile:1                   # field 个数
HEXISTS profile:1 email          # field 是否存在

3.List

list存储有序的数据,保存到数组中

4. Set

set存储无需的,不能重复的数据到容器中

5. ZSet

zset在set基础上,每个数据绑定1个数字值,用于自动排序用。

7.2 spring中使用

企业的spring项目中,使用redis最多的有两种方式StringRedisTemplate、@Cacheable

1.StringRedisTemplate

如下是一段redis配合mysql的数据查询操作,其中redisTemplate负责操作redis,而objectMapper负责做数据转换。 读:redis读取--->json->转换成对象;写:对象-->json字符串-->redis写入

public User getById(Long id) {
    String key = KEY_PREFIX + id;
    String json = redisTemplate.opsForValue().get(key);
    if (json != null) {
      try {
        return objectMapper.readValue(json, User.class);
      } catch (JsonProcessingException e) {
        throw new RuntimeException("Failed to parse JSON", e);
      }
    }
    // 缓存未命中,走mysql查询
    User user = userMapper.findById(id);
    if (user == null) {
      throw new RuntimeException("User not found");
    }
    try {
      redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(user), TTL_MINUTES, TimeUnit.MINUTES);
    } catch (JsonProcessingException e) {
      throw new RuntimeException("Failed to serialize user", e);
    }
    return user;
  }
  

2.@Cacheable

@Cacheable通过注解操作redis,其省略了很多代码,但是本质含义和上面方式一样。常用注解有两种,查询和删除。value和key拼接作为redis的实际key,例如参数id是1,则redis实际会执行get user::1去获取缓存结果。

  1. @Cacheable(value = "user", key = "#id")

  2. @CacheEvict(value = "user", key = "#id")

@Cacheable(value = "user", key = "#id")
public User getById(Long id) {
    User user = userMapper.findById(id);
    if (user == null) {
        throw new BusinessException(404, "用户不存在: " + id);
    }
    return user;
}

@CacheEvict(value = "user", key = "#id")
public void delete(Long id) {
    if (userMapper.deleteById(id) == 0) {
        throw new RuntimeException("用户不存在: " + id);
    }
}

八、RocketMQ

mq消息队列核心解决3个问题

1.异步:主流程不必等待非核心流程 2.解耦:订单服务不依赖短信服务 3.削峰:例如秒杀流量先进队列,慢慢执行

8.1 案例一:收发消息

主流程下单后,立即返回下单成功,其中短信通知、日志异步处理。

1.通信事件类

首先定义生产者、消费者类之间的通信类,其内部自定义属性即可。

package com.example.spring_demo.mq2;
public record Event(String userId, String orderId) {
  
}

2.生产者


package com.example.spring_demo.mq2;

import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.stereotype.Service;

@Service
public class Product {
  private final RocketMQTemplate rocketMQTemplate;

  public Product(RocketMQTemplate rocketMQTemplate) {
    this.rocketMQTemplate = rocketMQTemplate;
  }
  public void sendMsg(Event event) {
    rocketMQTemplate.convertAndSend("order-topic:create", event);
  }
}

3.消费者

消费者可以有多个,此处包含订单、日志消费者。给类添加注解@RocketMQMessageListener标记它属于mq的消费者,其中topic+selectorExpression组合唯一映射生产者的消息事件名order-topic:create。


/*订单消费者*/
package com.example.spring_demo.mq2;

import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.springframework.stereotype.Component;
import org.apache.rocketmq.spring.core.RocketMQListener;

@Component
@RocketMQMessageListener(
  topic = "order-topic",
  selectorExpression = "create",
  consumerGroup = "order-consumer-group"
)
public class OrderComsumer implements RocketMQListener<Event> {
  @Override
  public void onMessage(Event event) {
    System.out.println("收到订单消息: userId=" + event.userId() + ", orderId=" + event.orderId());
  }
}



/*日志消费者*/

package com.example.spring_demo.mq2;

import org.springframework.stereotype.Component;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;

@Component
@RocketMQMessageListener(
  topic = "order-topic",
  selectorExpression = "create",
  consumerGroup = "log-consumer-group"
)
public class LogComsumer implements RocketMQListener<Event> {
  
  @Override
  public void onMessage(Event event) {
    System.out.println("订单创建成功,记录日志");
  }
}

4.测试

其中下单执行后,立即处理结果返回,其中日志与订单处理消费者在mq队列中异步处理。


package com.example.spring_demo.controller;
import java.util.UUID;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.spring_demo.common.Result;
import com.example.spring_demo.mq2.Event;
import com.example.spring_demo.mq2.Product;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final Product product;
    public OrderController(OrderMqProducer orderMqProducer, Product product) {
        this.product = product;
        this.orderMqProducer = orderMqProducer;
    }
    @PostMapping("/demo2")
    public Result<Event> createDemoOrder2(
            @RequestParam(defaultValue = "1") Long userId) {
        Event event = new Event(userId.toString(), "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
        product.sendMsg(event);
        return Result.ok(event);
    }
}

九、Spring Cloud

spring cloud解决的是多个sping服务之间如何互相调用的难题,其架构设计如下

                    用户 / 前端
                         │
                         ▼
              ┌──────────────────────┐
              │  Gateway  :8080      │  统一入口
              └──────────┬───────────┘
                         │
         ┌───────────────┼───────────────┐
         ▼               ▼               ▼
   user-service    order-service    product-service
     :8081            :8082            :8083
   (Boot 应用)      (Boot 应用)      (Boot 应用)
         │               │               │
         └───────┬───────┴───────┬───────┘
                 ▼               ▼
              Redis          RocketMQ
                 │               │
                 └───────┬───────┘
                         ▼
                       MySQL
                 ┌───────────────┐
                 │ Nacos :8848   │  服务注册与发现
                 └───────────────┘

案例:假设order服务想调用user服务

1.user服务

user服务的controller层提供调用者需要的接口

package com.example.cloudminirpc.user.controller;

import java.util.List;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.cloudminirpc.common.Result;
import com.example.cloudminirpc.user.dto.UserCreateRequest;
import com.example.cloudminirpc.user.entity.User;
import com.example.cloudminirpc.user.service.UserService;

import jakarta.validation.Valid;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<User> list() {
        return userService.listAll();
    }

    @GetMapping("/{id}")
    public Result<User> getById(@PathVariable Long id) {
        return Result.ok(userService.getById(id));
    }

    @PostMapping
    public Result<User> create(@Valid @RequestBody UserCreateRequest request) {
        User user = new User();
        user.setUsername(request.getUsername());
        user.setEmail(request.getEmail());
        user.setPhone(request.getPhone());
        return Result.ok(userService.create(user));
    }

    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable Long id) {
        userService.delete(id);
        return Result.ok(null);
    }
}


1.order服务内部定义userClient

order服务内部定义user相关接口时,必须满足请求类型,路径path与参数一致,否则无法映射匹配到user的接口,方法名自定义即可。

@FeignClient(name = "user-service")
public interface UserClient {

    @GetMapping("/api/users/{id}")
    Result<UserDto> getUser(@PathVariable("id") Long id);

    @GetMapping("/api/users")
    Result<List<UserDto>> getUserByEmias();
}

2.order服务内部使用user服务

在order服务的Controller层直接调用这个接口

package com.example.cloudmini.order.controller;

import java.util.UUID;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.example.cloudmini.common.BusinessException;
import com.example.cloudmini.common.Result;
import com.example.cloudmini.order.client.UserClient;
import com.example.cloudmini.order.dto.OrderDto;
import com.example.cloudmini.order.dto.UserDto;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final UserClient userClient;

    public OrderController(UserClient userClient) {
        this.userClient = userClient;
    }

    @PostMapping("/demo")
    public Result<OrderDto> createDemoOrder(@RequestParam(defaultValue = "1") Long userId) {
        Result<UserDto> userResult = userClient.getUser(userId);
        if (userResult == null || userResult.getData() == null) {
            throw new BusinessException(404, "用户不存在: " + userId);
        }
        UserDto user = userResult.getData();
        String orderNo = "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
        OrderDto order = new OrderDto(
                orderNo,
                userId,
                user.getUsername(),
                "下单成功(Feign 已校验用户)"
        );
        return Result.ok(order);
    }

    @GetMapping("/batch")
    /**获取emain是.com的所有用户 */
    public Result<List<OrderDto>> createBatchOrders() {
        // return Result.ok(orders);
        Result<List<UserDto>> userResult = userClient.getUserByEmias();
        if (userResult == null || userResult.getData() == null) {
            throw new BusinessException(404, "用户不存在");
        }
        List<UserDto> users = userResult.getData();
        for (UserDto user : users) {
            String orderNo = "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
            OrderDto order = new OrderDto(orderNo, user.getId(), user.getUsername(), "下单成功(Feign 已校验用户)");
            orders.add(order);
        }
        return null;
    }
}

十、Dubbo RPC

rpc与spring cloud类似,它也可以实现多服务之间互相调用,spring Cloud:按路径打到对方 Controller 而RPC:按接口方法打到对方暴露的 RPC 实现

仍然是上面的案例,order服务想调用user服务。

1.定义user api

在user api中定义user服务需要提供的接口能力

UserRpcService.java


package com.example.cloudminirpc.user.api;

/**
 * Dubbo RPC 接口:order-service 通过此接口远程调用 user-service。
 * 与 HTTP Controller 分离,是 RPC 的「合同」。
 */
public interface UserRpcService {

    UserDto getById(Long id);
}

2.User服务内部实现细节

user服务内部定义的rpc只是给调用者用的。


package com.example.cloudminirpc.user.rpc;

import org.apache.dubbo.config.annotation.DubboService;

import com.example.cloudminirpc.user.api.UserDto;
import com.example.cloudminirpc.user.api.UserRpcService;
import com.example.cloudminirpc.user.entity.User;
import com.example.cloudminirpc.user.service.UserService;

/**
 * Dubbo Provider:把 user-service 的能力暴露为 RPC 接口。
 */
@DubboService
public class UserRpcServiceImpl implements UserRpcService {

    private final UserService userService;

    public UserRpcServiceImpl(UserService userService) {
        this.userService = userService;
    }

    @Override
    public UserDto getById(Long id) {
        try {
            User user = userService.getById(id);
            return new UserDto(user.getId(), user.getUsername(), user.getEmail(), user.getPhone());
        } catch (RuntimeException e) {
            return null;
        }
    }
}

3.order服务使用

order服务通过公共的中间UserRpcService接口直接调用它想要的能力。


package com.example.cloudminirpc.order.controller;

import java.util.UUID;

import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.example.cloudminirpc.common.BusinessException;
import com.example.cloudminirpc.common.Result;
import com.example.cloudminirpc.order.dto.OrderDto;
import com.example.cloudminirpc.user.api.UserDto;
import com.example.cloudminirpc.user.api.UserRpcService;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @DubboReference
    private UserRpcService userRpcService;

    @GetMapping("/health")
    public Result<String> health() {
        return Result.ok("order-rpc-service is running");
    }

    /**
     * 第 15 章演示:通过 Dubbo RPC 查用户,再创建订单(模拟)。
     * 对比 cloud-mini 的 Feign:这里不走 HTTP,走 Dubbo 二进制协议。
     */
    @PostMapping("/demo")
    public Result<OrderDto> createDemoOrder(@RequestParam(defaultValue = "1") Long userId) {
        UserDto user = userRpcService.getById(userId);
        if (user == null) {
            throw new BusinessException(404, "用户不存在: " + userId);
        }
        String orderNo = "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
        OrderDto order = new OrderDto(
                orderNo,
                userId,
                user.getUsername(),
                "下单成功(Dubbo RPC 已校验用户)"
        );
        return Result.ok(order);
    }
}

总之,order服务想调用user服务,就需要提供user api合同,合同里面放user暴露的能力;反之需要定义order api合同。