一、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管理需要特殊操作
- 在xxxCondig配置类顶部添加注解@Configuration
- 任意定义方法,方法的返回值就是工具类,并且添加注解@Bean。此时这个类就被Bean管理了
@Configuration
public class AppConfig {
@Bean
public Clock clock() {
return new Clock()
}
}
- 其它类使用工具类通过构造器注入
@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.properties 或 application.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支持配置全局异常拦截,任何地方的异常都可以被全局异常拦截。
- 添加全局异常类,头部添加@RestControllerAdvice
- 添加多个异常类方法,头部添加注解@ExceptionHandler(自定义异常类.class)
- 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如下
- set key value:存储数据
- get key:获取数据
- exists key:是否存在数据,0不存在,1存在
SET name "张三"
GET name # "张三"
SET counter 0
INCR counter # 1(计数器 +1)
DEL name # 删除 key
2.Hash
hash保存对象数据,其api如下
- hset name key1 value1 key2 value2 ...
- hget name:获取完整对象数据
- hget name key1:获取对象的属性是key1的
- 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去获取缓存结果。
-
@Cacheable(value = "user", key = "#id")
-
@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合同。