• [技术干货] SpringBoot实现websocket服务端及客户端的详细过程【转】
    一、WebSocket通信过程客户端构建一个websocket实例,并且为它绑定一个需要连接到的服务器地址,当客户端连接服务端的候,会向服务端发送一个http get报文,告诉服务端需要将通信协议切换到websocket,服务端收到http请求后将通信协议切换到websocket,同时发给客户端一个响应报文,返回的状态码为101,表示同意客户端协议转请求,并转换为websocket协议。以上过程都是利用http通信完成的,称之为websocket协议握手(websocket Protocol handshake),经过握手之后,客户端和服务端就建立了websocket连接,以后的通信走的都是websocket协议了。二、服务端实现1.pom文件添加依赖       <!--webSocket-->         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-websocket</artifactId>         </dependency>2.启用Springboot对WebSocket的支持package com.lby.websocket.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /**  * @author Liby  * @date 2022-04-25 16:18  * @description:  * @version:  */ @Configuration public class WebSocketConfig {     @Bean     public ServerEndpointExporter serverEndpointExporter() {         return new ServerEndpointExporter();     } } 3.核心配置:WebSocketServer因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketServer其实就相当于一个ws协议的Controller@ ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端, 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端新建一个ConcurrentHashMap webSocketMap 用于接收当前userId的WebSocket,方便传递之间对userId进行推送消息。下面是具体业务代码:package org.example.server; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; /* 启动项目后可通过 http://websocket.jsonin.com/ 进行测试 输入网址ws://localhost:8080/websocket/1211,检测是否能进行连接  */ @ServerEndpoint(value = "/websocket/{userId}") @Component public class WebSocket {     private final static Logger logger = LogManager.getLogger(WebSocket.class);     /**      * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的      */     private static int onlineCount = 0;     /**      * concurrent包的线程安全Map,用来存放每个客户端对应的MyWebSocket对象      */     private static ConcurrentHashMap<String, WebSocket> webSocketMap = new ConcurrentHashMap<>();     /**      * 与某个客户端的连接会话,需要通过它来给客户端发送数据      */     private Session session;     private String userId;     /**      * 连接建立成功调用的方法      */     @OnOpen     public void onOpen(Session session, @PathParam("userId") String userId) {         this.session = session;         this.userId = userId;         //加入map         webSocketMap.put(userId, this);         addOnlineCount();           //在线数加1         logger.info("用户{}连接成功,当前在线人数为{}", userId, getOnlineCount());         try {             sendMessage(String.valueOf(this.session.getQueryString()));         } catch (IOException e) {             logger.error("IO异常");         }     }     /**      * 连接关闭调用的方法      */     @OnClose     public void onClose() {         //从map中删除         webSocketMap.remove(userId);         subOnlineCount();           //在线数减1         logger.info("用户{}关闭连接!当前在线人数为{}", userId, getOnlineCount());     }     /**      * 收到客户端消息后调用的方法      *      * @param message 客户端发送过来的消息      */     @OnMessage     public void onMessage(String message, Session session) {         logger.info("来自客户端用户:{} 消息:{}",userId, message);         //群发消息         for (String item : webSocketMap.keySet()) {             try {                 webSocketMap.get(item).sendMessage(message);             } catch (IOException e) {                 e.printStackTrace();             }         }     }     /**      * 发生错误时调用      *      */     @OnError     public void onError(Session session, Throwable error) {         logger.error("用户错误:" + this.userId + ",原因:" + error.getMessage());         error.printStackTrace();     }     /**      * 向客户端发送消息      */     public void sendMessage(String message) throws IOException {         this.session.getBasicRemote().sendText(message);         //this.session.getAsyncRemote().sendText(message);     }     /**      * 通过userId向客户端发送消息      */     public void sendMessageByUserId(String userId, String message) throws IOException {         logger.info("服务端发送消息到{},消息:{}",userId,message);         if(StrUtil.isNotBlank(userId)&&webSocketMap.containsKey(userId)){             webSocketMap.get(userId).sendMessage("hello");         }else{             logger.error("用户{}不在线",userId);         }     }     /**      * 群发自定义消息      */     public void sendInfo(String message) throws IOException {         for (String item : webSocketMap.keySet()) {             try {                 webSocketMap.get(item).sendMessage(message);             } catch (IOException e) {                 e.printStackTrace();             }         }     }     public static synchronized int getOnlineCount() {         return onlineCount;     }     public static synchronized void addOnlineCount() {         WebSocket.onlineCount++;     }     public static synchronized void subOnlineCount() {         WebSocket.onlineCount--;     } } 三、客户端实现1.pom文件添加依赖        <dependency>             <groupId>org.java-websocket</groupId>             <artifactId>Java-WebSocket</artifactId>             <version>1.5.3</version>         </dependency>2.客户端实现代码package org.example; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; import java.net.URI; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /**  * @author kele  * @date 2024/2/19  **/ @Slf4j public class MyWebSocketClient extends WebSocketClient {     public MyWebSocketClient(URI serverUri) {         super(serverUri);     }     @SneakyThrows     @Override     public void onOpen(ServerHandshake data) {         try {             log.info("WebSocket连接已打开。");         }catch (Exception e){             log.error("onOpen error :{}",e.getMessage());         }     }     @SneakyThrows     @Override     public void onMessage(String message) {         try {             if (message != null && !message.isEmpty()) {                 log.info("收到消息: {}",message);             }         }catch (Exception e){             log.error("onMessage error : {}",message);         }     }     @Override     public void onClose(int code, String reason, boolean remote) {         log.info("WebSocket连接已关闭。");     }     @Override     public void onError(Exception ex) {         log.info("WebSocket连接发生错误:{}", ex.getMessage());     }     /**      * 连接定时检查      */     public void startReconnectTask(long delay, TimeUnit unit) {         System.out.println("WebSocket 心跳检查");         log.info("WebSocket 心跳检查");         // 以下为定时器,建议使用自定义线程池,或交给框架处理(spring)         ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();         executorService.scheduleWithFixedDelay(() -> {             // 检查逻辑:判断当前连接是否连通             if (!this.isOpen()) {                 System.out.println("WebSocket 开始重连......");                 log.info("WebSocket 开始重连......");                 // 重置连接                 this.reconnect();                 // 以下为错误示范                 //this.close();                 //this.connect();             }         }, 0, delay, unit);     } }3.开启客户端连接服务,并开启定时检查websocket连接package org.example; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.net.URI; import java.util.concurrent.TimeUnit; @Slf4j @Component public class Init implements Runnable {     public static MyWebSocketClient myWebSocketClient;     @PostConstruct     public void run () {         try {             //启动连接             log.info("连接websocket服务端");             log.info("项目启动");             // 服务地址             URI uri = new URI("ws://127.0.0.1:8077/websocket/123");             log.info("服务地址 -{}", uri);             // 创建客户端             myWebSocketClient = new MyWebSocketClient(uri);             // 建立连接             myWebSocketClient.connect();             // 开启 定时检查             myWebSocketClient.startReconnectTask(5, TimeUnit.SECONDS);         }catch (Exception e){             e.printStackTrace();         }     } }
  • [技术干货] SpringBoot中Get请求和POST请求接收参数示例详解【转】
    1、Get请求1.1 方法形参接收参数 这种方式一般适用参数比较少的情况,并且前后端参数名称必须保持一致@RestController @RequestMapping("/user") @Slf4j public class DemoController {     @GetMapping("/query")     public void getStudent(String name,String age) {         log.info("name:{}",name);         log.info("age:{}",age);     } }参数用 @RequestParam 标注,使用value属性指定参数名,required属性表示这个参数是否必传@RestController @RequestMapping("/user") @Slf4j public class DemoController {     @GetMapping("/query")     public void getStudent(@RequestParam(value = "name", required = false) String name,                            @RequestParam(value = "age", required = false) String age) {         log.info("name:{}",name);         log.info("age:{}",age);     } }1.2 实体类接收参数注意:Get 请求以实体类接收参数时,不能用 RequestParam 注解进行标注,因为不支持这样的方式获取参数。@RestController @RequestMapping("/demo") @Slf4j public class DemoController {     @GetMapping("/query")     public void getStudent(Student student) {         log.info("name:{}",student.getName());         log.info("age:{}",student.getAge());     } } @Data class Student{     private String name;     private Integer age; }1.3 通过HttpServletRequest接收参数@RestController @RequestMapping("/demo") @Slf4j public class DemoController {     @GetMapping("/query")     public void getStudent(HttpServletRequest request) {         String name = request.getParameter("name");         String phone = request.getParameter("age");         log.info("name:{}",name);         log.info("age:{}",age);     } }1.4 通过@PathVariable接收参数@RestController @RequestMapping("/demo") @Slf4j public class DemoController {     @GetMapping("/query/{name}/{age}")     public void getStudent(@PathVariable String name, @PathVariable String age) {         log.info("name:{}",name);         log.info("age:{}",age);     } }1.5 接收数组参数@RestController @RequestMapping("/demo") @Slf4j public class DemoController {     @GetMapping("/query")     public void getStudent(String[] names) {         Arrays.stream(names).forEach(System.out::println);     } }1.6 接受集合参数注意:SpringBoot 接收集合参数,必须用 @RequestParam 注解声明!@RestController @RequestMapping("/demo") @Slf4j public class DemoController {     @GetMapping("/query")     public void getStudent(@RequestParam List<String> names) {         names.forEach(System.out::println);     } }2、POST请求2.1 方法形参接收参数 前后端参数名称必须保持一致@RestController @RequestMapping("/user") @Slf4j public class DemoController {     @PostMapping("/save")     public void saveStudent(String name,String age) {         log.info("name:{}",name);         log.info("age:{}",age);     } }参数用 @RequestParam 标注,使用value属性指定参数名,required属性表示这个参数是否必传@RestController @RequestMapping("/user") @Slf4j public class DemoController {     @PostMapping("/save")     public void saveStudent(@RequestParam(value = "name", required = false) String name,                            @RequestParam(value = "age", required = false) String age) {         log.info("name:{}",name);         log.info("age:{}",age);     } }2.2 通过HttpServletRequest接收参数@RestController @RequestMapping("/demo") @Slf4j public class DemoController {     @PostMapping("/save")     public void saveStudent(HttpServletRequest request) {         String name = request.getParameter("name");         String phone = request.getParameter("age");         log.info("name:{}",name);         log.info("age:{}",age);     } }2.3 通过@PathVariable接收参数@RestController @RequestMapping("/demo") @Slf4j public class DemoController {     @PostMapping("/save/{name}/{age}")     public void saveStudent(@PathVariable String name, @PathVariable String age) {         log.info("name:{}",name);         log.info("age:{}",age);     } }2.4 通过param方式提交参数,以实体类接收参数直接以实体类可以接收param、form-data、 x-www-form-urlencoded 提交的参数@RestController @RequestMapping("/demo") @Slf4j public class DemoController {     @PostMapping("/save")     public void saveStudent(Student student) {         log.info("name:{}",student.getName());         log.info("age:{}",student.getAge());     } } @Data class Student{     private String name;     private Integer age; }2.5 请求体以JSON格式提交参数,通过 @RequestBody 注解接收参数接受实体类JSON参数@RestController @RequestMapping("/demo") @Slf4j public class DemoController {     @PostMapping("/save")     public void saveStudent(@RequestBody Student student) {         log.info("name:{}",student.getName());         log.info("age:{}",student.getAge());     } } @Data class Student{     private String name;     private Integer age; }2.6 通过 Map 接收参数@RestController @RequestMapping("/demo") @Slf4j public class DemoController {     @PostMapping("/save")     public void saveStudent(@RequestParam Map<String,Object> map) {         log.info("name:{}",map.get("name"));         log.info("age:{}",map.get("age"));     } }2.7 通过@RequestBody 接收一个参数@RestController @RequestMapping("/demo") @Slf4j public class DemoController {     @PostMapping("/save")     public void saveStudent(@RequestBody String name) {         log.info("name:{}",name);     } }
  • [技术干货] SpringBoot 整合 Avro 与 Kafka-转载
    优质博文:IT-BLOG-CN  【需求】:生产者发送数据至 kafka 序列化使用 Avro,消费者通过 Avro 进行反序列化,并将数据通过 MyBatisPlus 存入数据库。  一、环境介绍 【1】Apache Avro 1.8;【2】Spring Kafka 1.2;【3】Spring Boot 1.5;【4】Maven 3.5;  <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">   <modelVersion>4.0.0</modelVersion>     <groupId>com.codenotfound</groupId>   <artifactId>spring-kafka-avro</artifactId>   <version>0.0.1-SNAPSHOT</version>     <name>spring-kafka-avro</name>   <description>Spring Kafka - Apache Avro Serializer Deserializer Example</description>   <url>https://www.codenotfound.com/spring-kafka-apache-avro-serializer-deserializer-example.html</url>     <parent>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-parent</artifactId>     <version>1.5.4.RELEASE</version>   </parent>     <properties>     <java.version>1.8</java.version>       <spring-kafka.version>1.2.2.RELEASE</spring-kafka.version>     <avro.version>1.8.2</avro.version>   </properties>     <dependencies>     <!-- spring-boot -->     <dependency>       <groupId>org.springframework.boot</groupId>       <artifactId>spring-boot-starter</artifactId>     </dependency>     <dependency>       <groupId>org.springframework.boot</groupId>       <artifactId>spring-boot-starter-test</artifactId>       <scope>test</scope>     </dependency>     <!-- spring-kafka -->     <dependency>       <groupId>org.springframework.kafka</groupId>       <artifactId>spring-kafka</artifactId>       <version>${spring-kafka.version}</version>     </dependency>     <dependency>       <groupId>org.springframework.kafka</groupId>       <artifactId>spring-kafka-test</artifactId>       <version>${spring-kafka.version}</version>       <scope>test</scope>     </dependency>     <!-- avro -->     <dependency>       <groupId>org.apache.avro</groupId>       <artifactId>avro</artifactId>       <version>${avro.version}</version>     </dependency>   </dependencies>     <build>     <plugins>       <!-- spring-boot-maven-plugin -->       <plugin>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-maven-plugin</artifactId>       </plugin>       <!-- avro-maven-plugin -->       <plugin>         <groupId>org.apache.avro</groupId>         <artifactId>avro-maven-plugin</artifactId>         <version>${avro.version}</version>         <executions>           <execution>             <phase>generate-sources</phase>             <goals>               <goal>schema</goal>             </goals>             <configuration>               <sourceDirectory>${project.basedir}/src/main/resources/avro/</sourceDirectory>               <outputDirectory>${project.build.directory}/generated/avro</outputDirectory>             </configuration>           </execution>         </executions>       </plugin>     </plugins>   </build> </project> 二、Avro 文件 【1】Avro 依赖于由使用JSON定义的原始类型组成的架构。对于此示例,我们将使用Apache Avro入门指南中的“用户”模式,如下所示。该模式存储在src / main / resources / avro下的 user.avsc文件中。我这里使用的是 electronicsPackage.avsc。namespace 指定你生成 java 类时指定的 package 路径,name 表时生成的文件。  {"namespace": "com.yd.cyber.protocol.avro",  "type": "record",  "name": "ElectronicsPackage",  "fields": [      {"name":"package_number","type":["string","null"],"default": null},      {"name":"frs_site_code","type":["string","null"],"default": null},      {"name":"frs_site_code_type","type":["string","null"],"default":null},      {"name":"end_allocate_code","type":["string","null"],"default": null},      {"name":"code_1","type":["string","null"],"default": null},      {"name":"aggregat_package_code","type":["string","null"],"default": null}     ] } 【2】Avro附带了代码生成功能,该代码生成功能使我们可以根据上面定义的“用户”模式自动创建Java类。一旦生成了相关的类,就无需直接在程序中使用架构。这些类可以使用 avro-tools.jar 或项目是Maven 项目,调用 Maven Projects 进行 compile 自动生成 electronicsPackage.java 文件:如下是通过 maven 的方式   【3】这将导致生成一个 electronicsPackage.java 类,该类包含架构和许多 Builder构造 electronicsPackage对象的方法。   三、为 Kafka 主题生成 Avro消息 Kafka Byte 在其主题中存储和传输数组。但是,当我们使用 Avro对象时,我们需要在这些 Byte数组之间进行转换。在0.9.0.0版之前,Kafka Java API使用 Encoder/ Decoder接口的实现来处理转换,但是在新API中,这些已经被 Serializer/ Deserializer接口实现代替。Kafka附带了许多 内置(反)序列化器,但不包括Avro。为了解决这个问题,我们将创建一个 AvroSerializer类,该类Serializer专门为 Avro对象实现接口。然后,我们实现将 serialize() 主题名称和数据对象作为输入的方法,在本例中,该对象是扩展的 Avro对象 SpecificRecordBase。该方法将Avro对象序列化为字节数组并返回结果。这个类属于通用类,一次配置多次使用。  package com.yd.cyber.web.avro;   import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Map;   import org.apache.avro.io.BinaryEncoder; import org.apache.avro.io.DatumWriter; import org.apache.avro.io.EncoderFactory; import org.apache.avro.specific.SpecificDatumWriter; import org.apache.avro.specific.SpecificRecordBase; import org.apache.kafka.common.errors.SerializationException; import org.apache.kafka.common.serialization.Serializer;   /**  *  avro序列化类  * @author zzx  * @creat 2020-03-11-19:17  */ public class AvroSerializer<T extends SpecificRecordBase> implements Serializer<T> {     @Override     public void close() {}       @Override     public void configure(Map<String, ?> arg0, boolean arg1) {}       @Override     public byte[] serialize(String topic, T data) {         if(data == null) {             return null;         }         DatumWriter<T> writer = new SpecificDatumWriter<>(data.getSchema());         ByteArrayOutputStream byteArrayOutputStream  = new ByteArrayOutputStream();         BinaryEncoder binaryEncoder  = EncoderFactory.get().directBinaryEncoder(byteArrayOutputStream , null);         try {             writer.write(data, binaryEncoder);             binaryEncoder.flush();             byteArrayOutputStream.close();         }catch (IOException e) {             throw new SerializationException(e.getMessage());         }         return byteArrayOutputStream.toByteArray();     } } 四、AvroConfig 配置类 Avro 配置信息在 AvroConfig 配置类中,现在,我们需要更改,AvroConfig 开始使用我们的自定义 Serializer实现。这是通过将“ VALUE_SERIALIZER_CLASS_CONFIG”属性设置为 AvroSerializer该类来完成的。此外,我们更改了ProducerFactory 和KafkaTemplate 通用类型,使其指定 ElectronicsPackage 而不是 String。当我们有多个序列化的时候,这个配置文件需要多次需求,添加自己需要序列化的对象。  package com.yd.cyber.web.avro;   /**  * @author zzx  * @creat 2020-03-11-20:23  */ @Configuration @EnableKafka public class AvroConfig {       @Value("${spring.kafka.bootstrap-servers}")     private String bootstrapServers;       @Value("${spring.kafka.producer.max-request-size}")     private String maxRequestSize;       @Bean     public Map<String, Object> avroProducerConfigs() {         Map<String, Object> props = new HashMap<>();         props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);         props.put(ProducerConfig.MAX_REQUEST_SIZE_CONFIG, maxRequestSize);         props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);         props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, AvroSerializer.class);         return props;     }       @Bean     public ProducerFactory<String, ElectronicsPackage> elProducerFactory() {         return new DefaultKafkaProducerFactory<>(avroProducerConfigs());     }       @Bean     public KafkaTemplate<String, ElectronicsPackage> elKafkaTemplate() {         return new KafkaTemplate<>(elProducerFactory());     } } 五、通过 kafkaTemplate 发送消息 最后就是通过 Controller类调用 kafkaTemplate 的 send 方法接受一个Avro electronicsPackage对象作为输入。请注意,我们还更新了 kafkaTemplate 泛型类型。  package com.yd.cyber.web.controller.aggregation;   import com.yd.cyber.protocol.avro.ElectronicsPackage; import com.yd.cyber.web.vo.ElectronicsPackageVO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource;   /**  * <p>  * InnoDB free: 4096 kB 前端控制器  * </p>  *  * @author zzx  * @since 2020-04-19  */ @RestController @RequestMapping("/electronicsPackageTbl") public class ElectronicsPackageController {       //日誌     private static final Logger log = LoggerFactory.getLogger(ElectronicsPackageController.class);       @Resource     private KafkaTemplate<String,ElectronicsPackage> kafkaTemplate;       @GetMapping("/push")     public void push(){         ElectronicsPackageVO electronicsPackageVO = new ElectronicsPackageVO();         electronicsPackageVO.setElectId(9);         electronicsPackageVO.setAggregatPackageCode("9");         electronicsPackageVO.setCode1("9");         electronicsPackageVO.setEndAllocateCode("9");         electronicsPackageVO.setFrsSiteCodeType("9");         electronicsPackageVO.setFrsSiteCode("9");         electronicsPackageVO.setPackageNumber("9");         ElectronicsPackage electronicsPackage = new ElectronicsPackage();         BeanUtils.copyProperties(electronicsPackageVO,electronicsPackage);         //发送消息         kafkaTemplate.send("Electronics_Package",electronicsPackage);         log.info("Electronics_Package TOPIC 发送成功");     } } 六、从 Kafka主题消费 Avro消息反序列化 收到的消息需要反序列化为 Avro格式。为此,我们创建一个 AvroDeserializer 实现该 Deserializer接口的类。该 deserialize()方法将主题名称和Byte数组作为输入,然后将其解码回Avro对象。从 targetType类参数中检索需要用于解码的模式,该类参数需要作为参数传递给 AvroDeserializer构造函数。  package com.yd.cyber.web.avro;   import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.Arrays; import java.util.Map;   import org.apache.avro.generic.GenericRecord; import org.apache.avro.io.BinaryDecoder; import org.apache.avro.io.DatumReader; import org.apache.avro.io.DecoderFactory; import org.apache.avro.specific.SpecificDatumReader; import org.apache.avro.specific.SpecificRecordBase; import org.apache.kafka.common.errors.SerializationException; import org.apache.kafka.common.serialization.Deserializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory;   import javax.xml.bind.DatatypeConverter;   /**  *  avro反序列化  * @author fuyx  * @creat 2020-03-12-15:19  */ public class AvroDeserializer<T extends SpecificRecordBase> implements Deserializer<T> {     //日志系统     private static final Logger LOGGER = LoggerFactory.getLogger(AvroDeserializer.class);       protected final Class<T> targetType;       public AvroDeserializer(Class<T> targetType) {         this.targetType = targetType;     }     @Override     public void close() {}       @Override     public void configure(Map<String, ?> arg0, boolean arg1) {}       @Override     public T deserialize(String topic, byte[] data) {         try {             T result = null;             if(data == null) {                 return null;             }             LOGGER.debug("data='{}'", DatatypeConverter.printHexBinary(data));             ByteArrayInputStream in = new ByteArrayInputStream(data);             DatumReader<GenericRecord> userDatumReader = new SpecificDatumReader<>(targetType.newInstance().getSchema());             BinaryDecoder decoder = DecoderFactory.get().directBinaryDecoder(in, null);             result = (T) userDatumReader.read(null, decoder);             LOGGER.debug("deserialized data='{}'", result);             return result;         } catch (Exception ex) {             throw new SerializationException(                     "Can't deserialize data '" + Arrays.toString(data) + "' from topic '" + topic + "'", ex);         } finally {           }     } } 七、反序列化的配置类 我将反序列化的配置和序列化的配置都放置在 AvroConfig 配置类中。在 AvroConfig 需要被这样更新了AvroDeserializer用作值“VALUE_DESERIALIZER_CLASS_CONFIG”属性。我们还更改了 ConsumerFactory 和 ConcurrentKafkaListenerContainerFactory通用类型,以使其指定 ElectronicsPackage 而不是 String。将 DefaultKafkaConsumerFactory 通过1个新的创造 AvroDeserializer 是需要 “User.class”作为构造函数的参数。需要使用Class<?> targetType,AvroDeserializer 以将消费 byte[]对象反序列化为适当的目标对象(在此示例中为 ElectronicsPackage 类)。  @Configuration @EnableKafka public class AvroConfig {       @Value("${spring.kafka.bootstrap-servers}")     private String bootstrapServers;       @Value("${spring.kafka.producer.max-request-size}")     private String maxRequestSize;         @Bean     public Map<String, Object> consumerConfigs() {         Map<String, Object> props = new HashMap<>();         props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);         props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);         props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, AvroDeserializer.class);         props.put(ConsumerConfig.GROUP_ID_CONFIG, "avro");           return props;     }       @Bean     public ConsumerFactory<String, ElectronicsPackage> consumerFactory() {         return new DefaultKafkaConsumerFactory<>(consumerConfigs(), new StringDeserializer(),                 new AvroDeserializer<>(ElectronicsPackage.class));     }       @Bean     public ConcurrentKafkaListenerContainerFactory<String, ElectronicsPackage> kafkaListenerContainerFactory() {         ConcurrentKafkaListenerContainerFactory<String, ElectronicsPackage> factory =                 new ConcurrentKafkaListenerContainerFactory<>();         factory.setConsumerFactory(consumerFactory());           return factory;     }   } 八、消费者消费消息 消费者通过 @KafkaListener 监听对应的 Topic ,这里需要注意的是,网上直接获取对象的参数传的是对象,比如这里可能需要传入 ElectronicsPackage 类,但是我这样写的时候,error日志总说是返回序列化的问题,所以我使用 GenericRecord 对象接收,也就是我反序列化中定义的对象,是没有问题的。然后我将接收到的消息通过 mybatisplus 存入到数据库。  package com.zzx.cyber.web.controller.dataSource.intercompany;   import com.zzx.cyber.web.service.ElectronicsPackageService; import com.zzx.cyber.web.vo.ElectronicsPackageVO; import org.apache.avro.generic.GenericRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Controller;   import javax.annotation.Resource;   /**  * @desc:  * @author: zzx  * @creatdate 2020/4/1912:21  */ @Controller public class ElectronicsPackageConsumerController {       //日志     private static final Logger log  = LoggerFactory.getLogger(ElectronicsPackageConsumerController.class);       //服务层     @Resource     private ElectronicsPackageService electronicsPackageService;     /**      * 扫描数据测试      * @param genericRecordne      */     @KafkaListener(topics = {"Electronics_Package"})     public void receive(GenericRecord genericRecordne) throws Exception {         log.info("数据接收:electronicsPackage + "+  genericRecordne.toString());         //业务处理类,mybatispuls 自动生成的类         ElectronicsPackageVO electronicsPackageVO = new ElectronicsPackageVO();         //将收的数据复制过来         BeanUtils.copyProperties(genericRecordne,electronicsPackageVO);         try {             //落库             log.info("数据入库");             electronicsPackageService.save(electronicsPackageVO);         } catch (Exception e) {             throw new Exception("插入异常"+e);         }     } }  ————————————————                              版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                          原文链接:https://blog.csdn.net/zhengzhaoyang122/article/details/144169661 
  • [技术干货] Spring boot 中的 ThreadLocal 详解
    Java中的ThreadLocal是一个非常有用的工具类,以下是对其的详细解释:一、定义ThreadLocal是Java并发包(java.util.concurrent)中提供的一个类,它的主要作用是在多线程环境下为每个线程提供一个独立的变量副本,使得每个线程在访问ThreadLocal时获取到的都是自己的私有变量,而不是共享的同一个变量。换句话说,ThreadLocal能够隔离线程间的数据共享,提供线程级别的数据存储。二、作用ThreadLocal的出现主要是为了解决多线程环境下的数据共享问题。在传统的多线程编程中,多个线程之间共享数据通常是通过共享对象来实现的。但是,这种方式在处理多个线程之间需要共享大量数据时,会带来一些问题。多个线程同时修改共享数据时可能会出现竞争条件(race condition),导致数据的不一致性。如果多个线程需要访问共享数据,就需要进行频繁的同步操作,这会降低程序的性能。通过使用ThreadLocal,可以将需要共享的数据存储在每个线程的本地变量中,每个线程只能看到和修改自己的副本,而不会影响其他线程的副本。这样就可以避免多个线程同时修改同一份数据,避免了竞争条件和数据不一致性的问题。同时,由于每个线程都有自己的数据副本,不需要进行频繁的同步操作,提高了程序的性能。三、应用场景ThreadLocal常用于以下场景:线程上下文信息传递:例如在web应用中,服务器接收到请求后,需要在不同的过滤器、处理器链路中传递用户会话信息,此时可以将这些信息存放在ThreadLocal中。因为在Servlet容器中,每个HTTP请求都会被分配到一个单独的线程中处理。避免同步开销:对于那些只需要在单个线程内保持状态,不需要线程间共享的数据,使用ThreadLocal可以避免使用锁带来的性能损耗。数据库连接、事务管理:在多线程环境下,每个线程有自己的数据库连接,可以使用ThreadLocal存储当前线程的数据库连接对象,以确保线程安全。线程安全类:对于一些不是线程安全的类(如SimpleDateFormat),可以通过ThreadLocal为每个线程提供独立的实例,从而避免线程安全问题。四、示例以下是一个使用ThreadLocal保存用户的交易信息的例子:public class TransactionContext { private static ThreadLocal<Transaction> transactionThreadLocal = new ThreadLocal<>(); public static void setTransaction(Transaction transaction) { transactionThreadLocal.set(transaction); } public static Transaction getTransaction() { return transactionThreadLocal.get(); } public static void clear() { transactionThreadLocal.remove(); } } public class OrderService { public void processOrder(String orderId, String userId) { // 创建一个交易信息对象 Transaction transaction = new Transaction(orderId, userId); // 将交易信息存储到ThreadLocal TransactionContext.setTransaction(transaction); try { // 执行订单处理逻辑 validateOrder(); processPayment(); finalizeOrder(); } finally { // 清理ThreadLocal,防止内存泄露 TransactionContext.clear(); } } private void validateOrder() { // 获取当前线程的交易信息 Transaction transaction = TransactionContext.getTransaction(); // 订单校验逻辑... } private void processPayment() { Transaction transaction = TransactionContext.getTransaction(); // 支付处理逻辑... } private void finalizeOrder() { Transaction transaction = TransactionContext.getTransaction(); // 订单最终确认逻辑... } }在这个例子中,每个线程处理交易请求时,都会为该线程设置自己的交易上下文。通过ThreadLocal,可以确保每个线程的交易信息不会被其他并发线程篡改。在处理完订单后,需要在finally块中清理ThreadLocal,以防止内存泄露。总的来说,ThreadLocal是Java中一个非常有用的工具类,它可以帮助解决多线程环境下的数据共享问题。但是,在使用时也需要注意一些细节问题,如及时清理ThreadLocal以避免内存泄漏等。
  • [技术干货] spring boot3 和 spring boot2的区别
    Spring Boot 3与Spring Boot 2之间存在多个方面的显著区别,这些区别主要体现在Java版本依赖、模块化支持、Web框架、技术栈和依赖项更新、功能增强和改进等方面。以下是对这些区别的详细归纳:1. Java版本依赖Spring Boot 2:基于Java 8,同时也支持Java 9。这意味着在Spring Boot 2中,项目的编译和运行可以依赖于Java 8或Java 9。Spring Boot 3:将Java 11作为基准版本,并官方支持Java 17及更高版本。升级到Spring Boot 3需要项目使用Java 11或更高版本进行编译和运行,以便充分利用新版本Java带来的特性和改进。2. 模块化支持Spring Boot 2:在模块化支持方面相对有限,可能不完全满足现代应用程序对模块化的高要求。Spring Boot 3:更加注重模块化,提供更好的模块化支持。这使得开发人员能够更轻松地构建和维护模块化的应用程序,提高开发效率和可维护性。3. Web框架Spring Boot 2:默认使用Spring MVC作为Web框架。Spring MVC是一个基于Servlet的MVC框架,适用于构建传统的Web应用程序。Spring Boot 3:引入了对Spring WebFlux的支持。Spring WebFlux是一个非阻塞的、响应式的Web框架,适用于构建高性能的异步和事件驱动的应用程序。这一变化使得Spring Boot 3能够更好地支持现代Web应用的开发需求。4. 技术栈和依赖项更新Spring Boot 3相对于Spring Boot 2在技术栈和依赖项方面进行了全面的升级和更新。这包括Java版本的升级、Spring Framework版本的升级(从Spring Framework 5升级到Spring Framework 6),以及第三方库版本的更新。这些更新确保了Spring Boot 3与最新的技术和标准保持一致,为开发者提供了更强大、更灵活的开发环境。5. 功能增强和改进Spring Boot 3引入了一系列新功能和改进,以提高开发者的生产力和应用程序的性能。这些增强包括但不限于配置变化的改进、对GraalVM Native Image的支持(允许将Spring Boot应用程序编译成单个的、独立的可执行文件)、安全性改进、性能优化、改进的依赖管理、全新的启动器以及对Kotlin的支持等。此外,Spring Boot 3还可能废弃或移除了一些旧版本中的功能或库,如Apache ActiveMQ、Atomikos、EhCache2和HazelCast3的支持以及Jersey的移除等。这些变化要求开发者在升级过程中注意相应的调整和适配。6. 其他重要区别数据库访问:Spring Boot 3引入了对Spring Data R2DBC的支持,这是一个响应式的数据库访问框架,适用于构建基于事件驱动的应用程序。而Spring Boot 2默认使用Spring Data JPA进行数据库访问。配置属性:Spring Boot 3对配置属性的处理方式进行了改进,包括更清晰的错误消息和更灵活的属性绑定等,使得开发者可以更容易地理解和配置Spring Boot应用程序。综上所述,Spring Boot 3在多个方面相对于Spring Boot 2进行了显著的升级和改进,这些变化为开发者提供了更强大、更灵活的开发环境,并有助于构建更加高效、安全的现代应用程序。
  • [技术干货] spring boot 操作mongodb
    Spring Boot 提供了与 MongoDB 交互的简便方式,主要通过 Spring Data MongoDB 来实现。Spring Data MongoDB 是 Spring Data 项目的一部分,它简化了 MongoDB 数据的访问层(DAO层)的开发。下面将介绍如何在 Spring Boot 项目中集成和操作 MongoDB。1. 添加依赖首先,你需要在 Spring Boot 项目的 pom.xml(对于 Maven 项目)或 build.gradle(对于 Gradle 项目)中添加 Spring Data MongoDB 的依赖。Maven<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency>Gradleimplementation 'org.springframework.boot:spring-boot-starter-data-mongodb'2. 配置 MongoDB在 application.properties 或 application.yml 中配置 MongoDB 的连接信息。application.propertiesspring.data.mongodb.uri=mongodb://username:password@localhost:27017/yourdatabase # 或者使用以下方式分别配置 # spring.data.mongodb.host=localhost # spring.data.mongodb.port=27017 # spring.data.mongodb.database=yourdatabase # spring.data.mongodb.username=username # spring.data.mongodb.password=passwordapplication.ymlspring: data: mongodb: uri: mongodb://username:password@localhost:27017/yourdatabase # 或者 # host: localhost # port: 27017 # database: yourdatabase # username: username # password: password3. 创建实体类在 Spring Data MongoDB 中,你通常会将你的数据模型映射到 MongoDB 的文档上。你需要创建一个简单的 Java 类来表示 MongoDB 中的文档。import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; @Document(collection = "yourCollectionName") public class YourEntity { @Id private String id; private String name; // 省略 getter 和 setter }4. 创建仓库接口Spring Data MongoDB 允许你通过定义接口来创建数据访问对象(DAO)。你只需继承 MongoRepository 或 CrudRepository 并指定你的实体类和 ID 类型。import org.springframework.data.mongodb.repository.MongoRepository; public interface YourEntityRepository extends MongoRepository<YourEntity, String> { // 你可以在这里定义查询方法,但 Spring Data MongoDB 会为你实现基本的 CRUD 操作 }5. 使用仓库现在你可以在你的服务层或控制器中注入你的仓库接口,并使用它来执行数据库操作了。import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class YourEntityService { @Autowired private YourEntityRepository yourEntityRepository; public void addYourEntity(YourEntity yourEntity) { yourEntityRepository.save(yourEntity); } // 其他业务逻辑 }6. 运行和测试运行你的 Spring Boot 应用,并尝试使用你定义的接口进行 CRUD 操作。通过以上步骤,你可以在 Spring Boot 项目中轻松地集成和操作 MongoDB 数据库。Spring Data MongoDB 提供了一套丰富的功能来支持复杂的查询和映射,但上述步骤涵盖了最基础的部分,足以让你开始使用。
  • [技术干货] spring boot 自定义注解
    在Spring Boot中,自定义注解是一种强大的功能,它允许你定义自己的元数据,并通过AOP(面向切面编程)、Spring的Bean生命周期管理等功能来扩展或控制应用程序的行为。下面是如何在Spring Boot中创建一个自定义注解的基本步骤:1. 定义注解首先,你需要使用Java的@interface关键字来定义一个注解。注解可以包含元素(element),这些元素在注解被使用时必须或可以指定值。import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; // 定义注解的适用范围 @Target({ElementType.METHOD, ElementType.TYPE}) // 定义注解的保留策略,运行时有效 @Retention(RetentionPolicy.RUNTIME) public @interface MyCustomAnnotation { // 定义一个元素,使用时可以指定值 String value() default "defaultValue"; // 可以定义更多的元素... }2. 使用注解注解定义好后,就可以在你的代码中使用它了。比如,在Spring Boot的Controller或Service中的方法上使用:@RestController public class MyController { @MyCustomAnnotation(value = "someValue") @GetMapping("/test") public String testMethod() { return "This is a test method."; } }3. 处理注解要让自定义注解真正“工作”,你通常需要创建一个处理该注解的组件。这通常涉及到Spring AOP的使用,但也可以通过其他方式实现,比如实现BeanPostProcessor接口等。使用Spring AOP处理注解添加Spring AOP依赖(如果尚未添加):<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>创建Aspect类:import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component public class MyCustomAnnotationAspect { // 定义切点,匹配所有带有@MyCustomAnnotation注解的方法 @Pointcut("@annotation(com.example.MyCustomAnnotation)") public void myCustomAnnotationPointcut() {} // 在目标方法执行之前执行 @Before("myCustomAnnotationPointcut()") public void beforeAdvice(JoinPoint joinPoint) { MyCustomAnnotation annotation = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(MyCustomAnnotation.class); System.out.println("Before executing method: " + joinPoint.getSignature().getName() + " with value: " + annotation.value()); } }在这个例子中,每当一个方法被@MyCustomAnnotation注解标记时,beforeAdvice方法都会在该方法执行之前被调用,并打印出方法的名称和注解的值。结论通过以上步骤,你可以在Spring Boot项目中定义、使用和处理自定义注解。自定义注解是Spring Boot中非常强大的特性,可以用来实现各种自定义行为,比如日志记录、权限检查、事务管理等。
  • [技术干货] 【Spring Boot】Spring原理:Bean的作用域和生命周期-转载
     一. 知识回顾 在之前IOC/DI的学习中我们也用到了Bean对象,现在先来回顾一下IOC/DI的知识吧! 首先Spring IOC,也叫控制反转,简单来说就是依赖添加5大注解把该对象交给Spring来管理,Spring会把该对象放入IOC容器中,在接下来的调用中直接注入即可,注入也就是Spring DI操作了。 回顾一下,一共有以下五大注解:  1.1 回顾Spring IOC 类注解  @Controller(控制层注解) @Service(逻辑层注解) @Repository(数据层注解) @Component(总注解) @Configuration(插件注解) 然后就是有一点要注意的就是,@Component注解可以说是其他四个注解的父注解,就是其他注解底层都是依赖@Component来实现的,都可以使用@Component注解来代替使用,但是不能代替@Controller,因为别忘了Controller注解还有返回视图的作用,这是@Component注解所不具备的  方法注解  @Bean(方法注解) 该注解用于把方法交给Spring进行管理,但是必须和类注解连用  1.2 回顾Spring DI Spring DI就是把IOC容器里的东西拿出来进行使用,主要是@Autowired注解,主要有三种注入方式  属性注入(就是通过给成员变量进行注入) 构造注入(就是通过构造方法注入) Set方法注入 其实在实际运用中,使用属性注入基本上能满足90%的需求了.  Spring DI主要面试考的主要是拥有多个相同对象,注入时该如何保证?  主要提供了以下三大注解  @Primary(默认注入的方法) @Qualifier(加入要注入对象的方法名称) @Resource(要注入对象的名称) @Autowird与@Resource的区别  @Autowired是spring框架提供的注解,⽽@Resource是JDK提供的注解 @Autowired默认是按照类型注⼊,⽽@Resource是按照名称注⼊.相⽐于@Autowired来说,@Resource⽀持更多的参数设置,例如name设置,根据名称获取Bean。 1.3 回顾如何获取对象 Spring主要提供了两种方法获取Bean对象:  ApplicationContext(上下文) @Autowired 第二种是注入方式就不一一叙述了,主要是ApplicationContext获取Bean对象,其实就是调用了分类BeanFactory工厂来获取对象。 两者主要有以下两点区别:  继承关系和功能方⾯来说:Spring容器有两个顶级的接口:BeanFactory和 ApplicationContext。其中BeanFactory提供了基础的访问容器的能⼒,⽽ ApplicationContext属于BeanFactory的⼦类,它除了继承了BeanFactory的所有功能之外, 它还拥有独特的特性,还添加了对国际化⽀持、资源访问⽀持、以及事件传播等方⾯的⽀持.  从性能方⾯来说:ApplicationContext是⼀次性加载并初始化所有的Bean对象,也就是饿加载,⽽ BeanFactory是需要那个才去加载那个,也就是懒加载,因此更加轻量.(空间换时间)  好了,回顾完了Spring IOC/DI的知识点了,就该进入正题了。  二. Bean的作用域 Bean的作用域是指Bean在Spring框架中的某种行为模式.  主要有以下6种作用域  singleton:单例作用域 prototype:原型作用域(多例作用域) request:请求作用域 session:会话作用域 Application:全局作用域 websocket:HTTPWebSocket作用域 作用域    说明 singleton    每个SpringIoC容器内同名称的bean只有⼀个实例(单例)(默认) prototype    每次使用该bean时会创建新的实例(⾮单例) request    每个HTTP请求生命周期内,创建新的实例(web环境中) session    每个HTTPSession生命周期内,创建新的实例(web环境中) application    每个ServletContext生命周期内,创建新的实例(web环境中) websocket    每个WebSocket生命周期内,创建新的实例(web环境中) 单例作用域:多次访问,得到的都是同⼀个对象,并且 @Autowired 和 applicationContext.getBean() 也是同⼀个对象. 多例作用域:观察ContextDog,每次获取的对象都不⼀样(注⼊的对象在Spring容器启动时,就已经注⼊了,所以多次请求也不会发生变化) 请求作用域:在⼀次请求中, @Autowired 和 applicationContext.getBean() 也是同⼀个对象. 但是每次请求,都会重新创建对象 会话作用域:在⼀个session中,多次请求,获取到的对象都是同⼀个,换⼀个浏览器访问,发现会重新创建对象.(另⼀个Session) Application作用域:在⼀个应用中,多次访问都是同⼀个对象 注意:Applicationscope就是对于整个web容器来说,bean的作用域是ServletContext级别的.这个和 singleton有点类似,区别在于:Applicationscope是ServletContext的单例,singleton是⼀个 ApplicationContext的单例.在⼀个web容器中ApplicationContext可以有多个 三. Bean的生命周期 生命周期指的是⼀个对象从诞生到销毁的整个生命过程,我们把这个过程就叫做⼀个对象的生命周期. Bean的生命周期分为以下5个部分: 实例化(为Bean分配内存空间) 属性赋值(Bean注⼊和装配,⽐如 @AutoWired ) 初始化 执行各种通知,如 BeanNameAware , BeanFactoryAware ,ApplicationContextAware 的接口方法. 使用Bean 销毁Bean 销毁容器的各种方法,如 @PreDestroy , DisposableBean 接口方法, destroymethod. 实现的代码如下: @Component public class BeanLifeComponent implements BeanNameAware {     private UserComponent userComponent;          public BeanLifeComponent() {         System.out.println("执行构造函数");     }          @Autowired     public void setUserComponent(UserComponent userComponent) {         System.out.println("设置属性userComponent");         this.userComponent = userComponent;     }          @Override     public void setBeanName(String s) {         System.out.println("执行了 setBeanName 方法:" + s);     }          /**     * 初始化     */     @PostConstruct     public void postConstruct() {         System.out.println("执行 PostConstruct()");     }          public void use() {         System.out.println("执行了use方法");     }          /**     * 销毁前执行方法     */     @PreDestroy     public void preDestroy() {         System.out.println("执行:preDestroy()");     } } ————————————————                              版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                          原文链接:https://blog.csdn.net/smile_sundays/article/details/140303944 
  • [技术干货] BeanUtils.copyProperties() 方法的使用及潜在问题
    BeanUtils.copyProperties() 方法的使用及潜在问题BeanUtils.copyProperties() 是 Spring 框架中提供的一个非常便利的工具方法,用于将一个对象的属性值拷贝到另一个对象的相同属性中。这个方法大大减少了手写 getter 和 setter 方法的繁琐工作,使得代码更加简洁和易于维护。然而,尽管这个方法非常方便,但在实际使用中还是存在一些潜在的问题和限制:性能问题:当对象属性非常多,或者需要频繁进行拷贝操作时,BeanUtils.copyProperties() 的性能可能会成为瓶颈。因为它是通过反射机制来动态调用 getter 和 setter 方法的,这相对于直接代码访问字段要慢。类型转换问题:在进行属性拷贝时,如果源对象的属性值与目标对象的属性类型不兼容,BeanUtils.copyProperties() 可能会抛出异常或者进行不恰当的类型转换。这要求开发者在使用时需要非常小心,确保属性类型的一致性。空值处理:默认情况下,BeanUtils.copyProperties() 会将源对象中的 null 值也拷贝到目标对象中。在某些场景下,这可能不是预期的行为,因为开发者可能希望保留目标对象原有的非空值。深拷贝与浅拷贝:BeanUtils.copyProperties() 实现的是浅拷贝,即对于对象类型的属性,它只会拷贝对象的引用,而不是对象本身。这可能会导致源对象和目标对象之间的不期望的耦合。推荐使用 @Mapper(componentModel = "spring")在 Spring 应用中,尤其是在处理大量数据转换(DTO 转换、VO 转换等)时,推荐使用 MyBatis-Plus 或 MapStruct 这样的库来定义明确的映射逻辑。对于 MapStruct,@Mapper(componentModel = "spring") 注解使得 MapStruct 生成的映射器(Mapper)可以被 Spring 管理,从而实现依赖注入等功能。推荐使用 MapStruct 的原因包括:类型安全:MapStruct 在编译时生成映射代码,这意味着任何类型不匹配都会在编译时被捕获,而不是在运行时。高性能:由于是编译时生成的代码,MapStruct 的性能接近直接代码访问字段,远高于使用反射的 BeanUtils.copyProperties()。自定义映射逻辑:MapStruct 允许开发者在映射器接口中定义自定义的映射逻辑,这提供了极大的灵活性。支持复杂映射:MapStruct 可以处理复杂的映射场景,包括嵌套对象的映射、条件映射、循环引用等。易于测试:由于映射逻辑是明确定义的,因此更容易编写和维护单元测试。综上所述,虽然 BeanUtils.copyProperties() 在某些简单场景下非常有用,但在需要高性能、类型安全或复杂映射逻辑的应用中,推荐使用 MapStruct 这样的库来定义明确的映射逻辑。
  • [技术干货] Spring Boot接入Security权限认证服务
    在Spring Boot项目中接入Spring Security以提供权限认证服务是一种常见的做法。Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。以下是一个基本的步骤指南,用于在Spring Boot项目中集成Spring Security以实现权限认证服务。1. 添加Spring Security依赖首先,你需要在你的pom.xml(对于Maven项目)或build.gradle(对于Gradle项目)中添加Spring Security的依赖。Maven:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>Gradle:implementation 'org.springframework.boot:spring-boot-starter-security'2. 配置Spring Security接下来,你需要配置Spring Security。你可以通过编写一个配置类来扩展WebSecurityConfigurerAdapter类(注意:从Spring Security 5.4开始,推荐使用更简洁的SecurityFilterChain配置方式,但这里先展示旧方式以便理解)。示例配置类(旧方式):import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/home").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("{noop}password").roles("USER"); } }注意:{noop}前缀表示密码未加密,仅用于示例。在生产环境中,你应该使用加密的密码。3. 自定义用户详情服务(可选)对于更复杂的用户认证场景,你可能需要实现自定义的UserDetailsService来加载用户信息。import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 实现根据用户名加载用户信息 // 这里只是一个示例,实际项目中你可能需要从数据库加载用户信息 return new org.springframework.security.core.userdetails.User(username, "password", new ArrayList<>()); } }然后,在你的SecurityConfig类中配置这个服务。4. 使用JWT或OAuth2进行认证(可选)对于需要无状态认证(如RESTful API)的应用,你可能会考虑使用JWT(JSON Web Tokens)或OAuth2。Spring Security提供了对JWT和OAuth2的支持,但可能需要额外的依赖和配置。5. 测试和部署最后,确保你的应用正确实现了权限认证功能。你可以通过模拟不同的用户请求来测试你的安全配置。一旦测试通过,你就可以将你的应用部署到生产环境了。记住,安全是一个持续的过程,你应该定期审查和更新你的安全策略。
  • [技术干货] Spring cloud处理服务器雪崩 —— Hystrix
    一、服务雪崩效应的定义服务雪崩效应是一种在大型互联网项目中常见的现象,它指的是由于某个服务出现问题或故障,导致其所依赖的其他服务也无法正常运行,进而引发整个系统多个服务同时失效,形成一个连锁反应的状况。具体来说,当一个或多个服务出现故障或缓慢响应时,这些服务无法及时地进行异常恢复,或者由于资源等原因无法承受更高的负载压力。故障服务所依赖的其他服务,因为接收到了来自该故障服务的请求而无法及时处理自己的任务,这些服务可能会开始积累未完成的工作。最终,整个系统中的多个服务都由于负载的不均衡而发生故障,无法正常运行,导致用户无法访问各项服务,并且整个系统无法进行任何操作,从而造成极大损失。二、服务雪崩效应形成的原因服务雪崩效应的形成可以由多种原因造成,主要包括以下几个方面:服务提供者不可用:服务提供者由于硬件故障、程序错误、缓存击穿等原因导致服务不可用。重试加大流量:在服务提供者不可用后,用户由于忍受不了长时间的等待,而不断刷新页面或提交表单,服务的调用端也存在大量服务异常后的重试逻辑,这些都会进一步加大请求流量。服务调用者不可用:当服务调用者使用同步调用时,会产生大量的等待线程占用系统资源。一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态,进而引发服务雪崩效应。三、服务雪崩的应对策略针对服务雪崩效应,可以采取以下应对策略来预防和应对:流量控制:通过限制业务访问的QPS(每秒查询率),避免服务因流量的突增而故障。优化代码和缓存:通过优化代码,减少不必要的计算和数据库查询,提高服务器的处理速度;使用缓存来减少对数据库的访问压力,提高服务器的响应速度。资源隔离:对依赖服务进行分类,使用独立的线程池或资源池来隔离不同的服务调用,避免一个服务的故障影响到其他服务。熔断和降级:在服务出现故障或性能下降时,通过熔断和降级策略来保护系统,避免整个系统崩溃。熔断器模式可以统计业务执行的异常比例,如果超出阈值则熔断该业务,拦截访问该业务的一切请求。监控和告警:通过监控服务器的性能指标,及时发现并处理异常情况,避免服务雪崩的发生。同时,设置告警机制,当服务器性能达到阈值时,及时通知相关人员进行处理。四、Spring Cloud微服务防雪崩利器HystrixHystrix是Netflix开发的一个用于处理分布式系统的延迟和容错的开源库,它提供了一种优雅的方式来处理服务雪崩效应。Hystrix通过以下机制来防止服务雪崩:资源隔离:Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离,避免一个服务的故障影响到其他服务。这种资源隔离的方式类似于货船中的舱壁隔离模式,可以减少风险。熔断器模式:Hystrix的熔断器模式可以统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。当熔断器开关打开时,请求被禁止通过,直到经过一段时间后熔断器自动进入半开状态,允许部分请求通过以检查服务是否恢复。命令模式:Hystrix使用命令模式来包裹具体的服务调用逻辑,并在命令模式中添加了服务调用失败后的降级逻辑。当服务调用失败或超时时,Hystrix会执行降级逻辑,并返回降级结果,从而避免服务雪崩。综上所述,Hystrix通过资源隔离、熔断器模式和命令模式等机制,为Spring Cloud微服务提供了强大的防雪崩能力,帮助开发者构建更加稳定和可靠的分布式系统。
  • [技术干货] JAVA利用Freemarker模版动态生成并导出Word文档
    在Java中,利用Freemarker模板引擎动态生成并导出Word文档通常涉及几个步骤:准备模板、填充数据、生成文档内容,并最终将内容保存为Word文档。但需要注意的是,Freemarker本身并不直接支持Word文档的格式(如.docx),它主要用于生成文本内容(如HTML、XML、文本文件等)。因此,为了生成Word文档,我们通常需要结合其他库(如Apache POI)来处理Word文档的生成。以下是一个基本的步骤说明,展示如何使用Freemarker生成内容,并使用Apache POI将这些内容写入Word文档:步骤 1: 添加依赖首先,确保你的项目中包含了Freemarker和Apache POI的依赖。如果你使用Maven,可以在pom.xml中添加如下依赖:<!-- Freemarker --> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.31</version> </dependency> <!-- Apache POI --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.2</version> </dependency>步骤 2: 准备Freemarker模板准备一个Freemarker模板,假设你有一个名为template.ftl的文件,内容可能如下:<html> <body> <h1>${title}</h1> <p>${content}</p> </body> </html>注意:虽然这里用的是HTML格式,但你可以根据需要调整模板来适配Word文档(如使用简单的文本格式或XML格式,Apache POI能够处理这两种格式)。步骤 3: 填充模板并生成内容使用Freemarker API填充模板,并获取生成的字符串内容。import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import freemarker.template.TemplateExceptionHandler; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.Map; public class FreemarkerUtil { public static String processTemplate(String templateName, Map<String, Object> dataModel) throws IOException, TemplateException { Configuration cfg = new Configuration(Configuration.VERSION_2_3_31); cfg.setDirectoryForTemplateLoading(new File("/path/to/templates")); cfg.setDefaultEncoding("UTF-8"); cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); Template template = cfg.getTemplate(templateName); Writer out = new StringWriter(); template.process(dataModel, out); return out.toString(); } }步骤 4: 使用Apache POI生成Word文档由于Freemarker直接生成的是HTML或文本内容,你需要将其转换为Word文档的内容。这里有一个简化的方法,即将HTML内容作为Word文档的一部分插入,但注意Word处理HTML的能力有限。更复杂的处理可能需要直接操作XWPFDocument(Apache POI的Word处理类),这通常涉及更多的代码和逻辑来构建文档的结构。// 这里仅提供概念性代码,实际将HTML转换为Word内容需要更多工作 // 可能需要使用额外的库如OpenHTMLToPDF(然后PDF转Word,但这通常不推荐) // 或直接构建XWPFDocument对象替代方案考虑到直接操作Word文档(.docx)的复杂性,如果你主要处理的是文本内容,并且不需要复杂的Word格式,你可以考虑使用纯文本文件或RTF文件作为输出格式。如果确实需要Word格式,并且内容相对简单,你可以考虑使用Docx4j或其他专门处理Word文档的库。对于复杂的Word文档生成,直接构建XWPFDocument或使用专门的模板引擎(如Aspose.Words for Java)可能是更好的选择。
  • [技术干货] Spring Cloud Gateway限流的缺陷
    Spring Cloud Gateway限流在提供高效、灵活和自适应的限流能力时,也存在一些缺陷,主要包括以下几个方面:1. 配置繁琐学习成本高:Spring Cloud Gateway的限流配置相对于其他限流组件而言较为复杂,需要按照一定的规则配置多个过滤器、路由等,这对于不熟悉其配置方式的开发者来说,需要一定的学习成本。配置复杂度:在实际应用中,为了实现对不同API、不同用户或不同业务场景的个性化限流,可能需要进行复杂的配置,这增加了系统的复杂性和维护难度。2. 算法限制令牌桶算法的局限性:虽然Spring Cloud Gateway通常使用令牌桶算法进行限流,该算法在应对短时间内的突发流量时表现较好,但对于长时间内的流量波动,限流效果可能不如预期。此外,令牌桶算法的实现也依赖于具体的参数设置,如令牌桶的容量、填充速率等,这些参数的调整需要一定的经验和技巧。突刺现象:在某些情况下,如果突发流量超过了令牌桶的容量,可能会导致后续的请求被直接拒绝,即使这些请求在后续的时间内是合理的。这种现象被称为“突刺现象”,它会影响用户体验和系统的可用性。3. 性能问题限流对性能的影响:限流本身会对系统性能造成一定的影响,因为系统需要额外的计算资源来处理限流逻辑。如果限流算法的实现不够高效,或者在高并发场景下处理不当,可能会导致限流成为系统的瓶颈,从而影响整体性能。资源消耗:在某些情况下,为了支持复杂的限流策略,系统可能需要消耗更多的计算资源、内存和存储资源。这可能会增加系统的运营成本和维护难度。4. 依赖外部系统Redis依赖:Spring Cloud Gateway的限流功能通常依赖于Redis等外部系统来存储和管理令牌信息。这意味着如果Redis等外部系统出现故障或性能问题,可能会影响限流功能的正常运行。此外,与外部系统的交互也会增加系统的复杂性和维护难度。综上所述,虽然Spring Cloud Gateway限流具有许多优势,但在实际应用中也需要关注其潜在的缺陷和问题。为了充分发挥其限流能力并避免潜在的风险,建议开发者在配置和使用过程中充分考虑系统的实际需求和场景特点,并结合具体的业务场景进行灵活配置和优化。
  • [技术干货] 分布式全局唯一id在spring boot中的用法
    分布式全局唯一ID简介分布式全局唯一ID(Distributed Globally Unique Identifier, DGUID)是在分布式系统中用于唯一标识数据、消息、HTTP请求等的标识符。由于分布式系统可能涉及多个节点、多个服务、甚至跨地域的部署,传统的数据库自增主键或单机系统的唯一ID生成方式已无法满足需求。因此,需要一种能够在全局范围内保证唯一性的ID生成机制。分布式全局唯一ID的特点全局唯一性:生成的ID在整个分布式系统中必须是唯一的,不能出现重复。高可用性:ID生成系统作为基础系统,必须具有高可用性,避免因单点故障导致整个系统不可用。趋势递增:在某些场景下,需要ID具有一定的趋势递增性,以便于数据库索引和排序。单调递增:在某些特殊需求下,需要保证生成的ID是单调递增的。信息安全:ID的生成应避免暴露系统内部信息,如MAC地址等,以防止信息泄露。Spring Boot中的解决方案在Spring Boot中,实现分布式全局唯一ID的生成有多种方案,以下是一些常见的解决方案:UUID简介:UUID(Universally Unique Identifier)是在一定的范围内(从特定的名字空间到全球)唯一的机器生成的标识符。UUID是16字节128位长的数字,通常以36字节的字符串表示。优点:性能好,效率高,无需网络请求,直接本地生成。缺点:无序,无法保证趋势递增;字符串存储,查询效率慢,存储空间大;信息不安全,基于MAC地址生成UUID的算法可能会造成MAC地址泄露。数据库自增主键+分布式策略简介:利用数据库的自增主键特性,结合分布式策略(如设置不同的起始值和步长)来生成全局唯一的ID。优点:实现简单,数字ID排序、搜索、分页等操作有利。缺点:单点故障风险,高并发时性能瓶颈;分库分表时自增ID会重复,需要复杂的策略来保证唯一性。Redis生成ID简介:利用Redis的原子操作(如INCR和INCRBY)来实现全局唯一ID的生成。Redis是单线程的,因此其操作本身是线程安全的。优点:性能高,不依赖于数据库;灵活方便,可以根据需要自定义ID的生成规则。缺点:占用带宽,每次生成ID都需要向Redis发送请求;Redis重启时数据可能会丢失(虽然可以通过其他机制来保证数据的持久性)。雪花算法(Snowflake)简介:雪花算法是Twitter开源的一种生成唯一ID的算法。它通过将64位的长整型数字分为多个部分,分别表示时间戳、数据中心ID、机器ID和序列号等,以此来保证ID的唯一性和趋势递增性。优点:生成的ID全局唯一,趋势递增;性能高,适合分布式系统。缺点:实现相对复杂;需要合理规划数据中心ID和机器ID的分配。自定义算法根据具体业务需求和系统架构,可以自定义全局唯一ID的生成算法。例如,结合时间戳、随机数、业务标识等元素来生成ID。优点:灵活性高,可以根据具体需求进行优化。缺点:需要投入更多的开发成本和维护成本。在Spring Boot项目中,可以根据项目的具体需求和环境选择合适的方案来实现分布式全局唯一ID的生成。例如,如果系统对ID的唯一性和性能要求较高,可以考虑使用雪花算法或Redis生成ID的方案;如果系统规模较小且对ID的唯一性要求不是特别严格,可以使用UUID或数据库自增主键+分布式策略的方案。
  • [技术干货] Spring boot 慢业务的分析处理过程
    在Spring Boot中,如果某个业务执行得很慢,可以通过一系列步骤来逐步分析问题所在。以下是一个详细的分析流程,包括可能的原因和对应的排查方法:1. 初步观察和日志分析步骤:查看日志:首先检查应用程序的日志文件,特别是与业务执行相关的部分。注意任何异常、错误或警告信息,这些信息可能是导致性能问题的直接原因。观察响应时间:通过日志或监控工具观察请求的响应时间,确定是哪个环节或组件导致了延迟。例子:日志中可能显示数据库查询耗时过长,或者某个特定的服务调用响应慢。2. 监控和性能指标步骤:使用Spring Boot Actuator:如果项目中已经集成了Spring Boot Actuator,可以利用其提供的端点(如/metrics)来查看系统的性能指标,如CPU使用率、内存占用、HTTP请求响应时间等。集成外部监控工具:如Prometheus、Grafana等,这些工具可以提供更详细和可视化的监控数据。例子:通过Actuator的/metrics端点,发现数据库查询的响应时间普遍较高。3. 深入代码分析步骤:审查代码:对执行慢的业务逻辑进行代码审查,查看是否有不必要的复杂操作、循环嵌套、大量计算或IO操作等。性能剖析:使用性能剖析工具(如JProfiler、YourKit Java Profiler等)对应用程序进行剖析,以获取更详细的性能数据,如方法执行时间、对象创建情况等。例子:通过性能剖析,发现某个服务方法内部有多个复杂的数据库查询,且这些查询之间存在不必要的重复计算。4. 数据库性能调优步骤:优化SQL查询:对性能瓶颈的SQL查询进行优化,如使用索引、优化查询逻辑、减少返回的数据量等。检查数据库连接:确保数据库连接池配置合理,没有过多的连接等待或超时。监控数据库性能:使用数据库监控工具(如MySQL的Performance Schema)来监控查询性能,查找慢查询并进行优化。例子:通过分析数据库查询日志,发现某个查询因为没有使用索引而导致了全表扫描,通过添加合适的索引后性能显著提升。5. 外部服务依赖步骤:检查外部服务响应:如果业务逻辑依赖于外部服务(如REST API、消息队列等),检查这些服务的响应时间和稳定性。优化服务调用:考虑使用异步调用、缓存结果或优化服务端的性能。例子:发现业务逻辑中频繁调用一个外部API,但该API的响应时间较长。通过增加本地缓存或改用更快速的替代服务,减少了等待时间。6. 并发和资源限制步骤:检查线程池和并发设置:确保Spring Boot的线程池和数据库连接池配置能够满足当前的业务需求。优化JVM参数:根据应用程序的内存和CPU使用情况,调整JVM的启动参数,如堆大小、垃圾回收策略等。例子:通过调整Spring Boot的线程池大小,增加了同时处理的请求数,从而减少了请求的平均响应时间。7. 总结和优化步骤:总结问题:根据以上步骤的分析结果,总结导致业务执行慢的根本原因。实施优化:根据问题原因制定相应的优化方案,并在测试环境中验证其效果。持续监控:在优化后持续监控系统的性能指标,确保问题得到根本解决,并准备应对可能出现的新问题。通过以上步骤,可以系统地分析和解决Spring Boot中业务执行慢的问题。