• [技术干货] Java 内存溢出排查优化实战:彻底干掉臭名昭著的 OOM-转载
    Java 内存溢出排查优化实战:彻底干掉臭名昭著的 OOMOutOfMemoryError,也就是臭名昭著的 OOM(内存溢出),相信很多球友都遇到过,相对于常见的业务异常,如数组越界、空指针等,OOM 问题 更难难定位和解决。这篇内容就以之前碰到的一次线上内存溢出的定位、解决问题的方式展开;希望能对碰到类似问题的球友带来思路和帮助。主要从表现-->排查-->定位-->解决 四个步骤来分析和解决问题。内存溢出和内存泄露在 Java 中,和内存相关的问题主要有两种,内存溢出和内存泄漏。内存溢出(Out Of Memory):就是申请内存时,JVM 没有足够的内存空间。通俗说法就是去蹲坑发现坑位满了。内存泄露(Memory Leak):就是申请了内存,但是没有释放,导致内存空间浪费。通俗说法就是有人占着茅坑不拉屎。内存溢出在 JVM 的内存区域中,除了程序计数器,其他的内存区域都有可能发生内存溢出。大家都知道,Java 堆中存储的都是对象,或者叫对象实例,那只要我们不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么就一定会产生内存溢出。比如说运行下面这段代码:public class OOM {    public static void main(String[] args) {        List<Object> list = new ArrayList<>();        while (true) {            list.add(new Object());        }    }}运行程序的时候记得设置一下 VM 参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError,限制堆内存大小为 20M,并且不允许扩展,并且当发生 OOM 时 dump 出当前内存的快照。运行结果如下:请看原文我们在讲运行时数据区的时候也曾讲过。内存泄露内存泄露是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。简单来说,就是应该被垃圾回收的对象没有回收掉,导致占用的内存越来越多,最终导致内存溢出。在上图中:对象 X 引用对象 Y,X 的生命周期比 Y 的生命周期长,Y 生命周期结束的时候,垃圾回收器不会回收对象 Y。来看下面的例子:public class MemoryLeak {    public static void main(String[] args) {      try{          Connection conn =null;          Class.forName("com.mysql.jdbc.Driver");          conn =DriverManager.getConnection("url","","");          Statement stmt =conn.createStatement();          ResultSet rs =stmt.executeQuery("....");      } catch(Exception e){//异常日志      } finally {        // 1.关闭结果集 Statement        // 2.关闭声明的对象 ResultSet        // 3.关闭连接 Connection    }  }}创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收。这样就会导致内存泄露,最终导致内存溢出。换句话说,内存泄露不是内存溢出,但会加快内存溢出的发生。内存溢出后的表象之前生产环境爆出的内存溢出问题会随着业务量的增长,出现的频次也越来越高。应用程序的业务逻辑非常简单,就是从 Kafka 中将数据消费下来,然后批量的做持久化操作。OOM 现象则是随着 Kafka 的消息越多,出现异常的频次就越快。由于当时还有其他工作所以只能让运维做重启,并且监控好堆内存以及 GC 情况。不得不说,重启大法真的好,能解决大量的问题,但不是长久之计。内存泄露的排查于是我们想根据运维之前收集到的内存数据、GC 日志尝试判断哪里出现了问题。结果发现老年代的内存使用就算是发生 GC 也一直居高不下,而且随着时间推移也越来越高。结合 jstat 的日志发现就算是发生了 FGC,老年代也回收不了,内存已经到顶。甚至有几台应用 FGC 达到了上百次,时间也高的可怕。这说明应用的内存使用肯定是有问题的,有许多赖皮对象始终回收不掉。内存泄露的定位由于生产上的内存 dump 文件非常大,达到了几十 G。也和我们生产环境配置的内存太大有关。所以导致想使用 MAT 分析需要花费大量时间。MAT 是 Eclipse 的一个插件,也可以单独使用,可以用来分析 Java 的堆内存,找出内存泄露的原因。因此我们就想是否可以在本地复现,这样就好定位的多。为了尽快的复现问题,我将本地应用最大堆内存设置为 150M。然后在消费 Kafka 那里 Mock 了一个 while 循环一直不断的生成数据。同时当应用启动之后利用 VisualVM 连上应用实时监控内存、GC 的使用情况。结果跑了 10 几分钟内存使用并没有什么问题。根据图中可以看出,每一次 GC 内存都能有效的回收,所以并没有复现问题。没法复现问题就很难定位。于是我们就采用了一种古老的方法——review 代码,发现生产的逻辑和我们用 while 循环 Mock 的数据还不太一样。果然 review 代码是保障程序性能的第一道防线,诚不欺我。大家在写完代码的时候,尽量也要团队 review 一次。后来查看生产日志发现每次从 Kafka 中取出的都是几百条数据,而我们 Mock 时每次只能产生一条。为了尽可能的模拟生产情况便在服务器上跑了一个生产者程序,一直源源不断的向 Kafka 中发送数据。果然不出意外只跑了一分多钟内存就顶不住了,观察下图发现 GC 的频次非常高,但是内存的回收却是相形见拙。同时后台也开始打印内存溢出了,这样便复现出了问题。内存泄露的解决从目前的表现来看,就是内存中有许多对象一直存在强引用关系导致得不到回收。于是便想看看到底是什么对象占用了这么多的内存,利用 VisualVM 的 HeapDump 功能,就可以立即 dump 出当前应用的内存情况。结果发现 com.lmax.disruptor.RingBuffer 类型的对象占用了将近 50% 的内存。看到这个包自然就想到了 Disruptor 环形队列了。Disruptor 是一个高性能的异步处理框架,它的核心思想是:通过无锁的方式来实现高性能的并发处理,其性能是高于 JDK 的 BlockingQueue 的。再次 review 代码发现:从 Kafka 里取出的 700 条数据是直接往 Disruptor 里丢的。这里也就能说明为什么第一次模拟数据没复现问题了。模拟的时候是一个对象放进队列里,而生产的情况是 700 条数据放进队列里。这个数据量就是 700 倍的差距啊。而 Disruptor 作为一个环形队列,在对象没有被覆盖之前是一直存在的。我也做了一个实验,证明确实如此。我设置队列大小为 8 ,从 0~9 往里面写 10 条数据,当写到 8 的时候就会把之前 0 的位置覆盖掉,后面的以此类推(类似于 HashMap 的取模定位)。所以在生产环境上,假设我们的队列大小是 1024,那么随着系统的运行最终会导致 1024 个位置上装满了对象,而且每个位置都是 700 个!于是查看了生产环境上 Disruptor 的 RingBuffer 配置,结果是:1024*1024。这个数量级就非常吓人了。为了验证是否是这个问题,我在本地将该值设为 2 ,一个最小值试试。同样的 128M 内存,也是通过 Kafka 一直源源不断的取出数据。通过监控如下:跑了 20 几分钟系统一切正常,每当一次 GC 都能回收大部分内存,最终呈现锯齿状。这样问题就找到了,不过生产上这个值具体设置多少还得根据业务情况测试才能知道,但原有的 1024*1024 是绝对不能再使用了。小结虽然到了最后也就改了一行代码(还没改,直接修改配置),但这个排查过程我觉得是很有意义的。也会让大部分觉得 JVM 这样的黑盒难以下手的球友有一个直观感受。同时也得感叹 Disruptor 东西虽好,也不能乱用哦!参考资料内存泄露的排查内存溢出和内存泄露————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/lilinhai548/article/details/147757180
  • [技术干货] Java分层开发必知:PO、BO、DTO、VO、POJO概念详解-转载
    引言  在Java企业级开发中,我们经常会遇到POJO、PO、DTO、BO、VO等各种对象概念,这些看似相似的术语常常让开发者感到困惑。本文将深入解析这些核心概念的区别与联系,并通过代码示例展示它们在实际项目中的正确使用方式。一、核心概念与定义1、PO(Persistent Object,持久化对象)定义:PO与数据库表结构一一对应,每个字段映射表中的一列,通常由ORM框架(如MyBatis、Hibernate)自动生成作用:用于数据持久化操作,如增删改查(CRUD),仅包含数据,不涉及业务逻辑MyBatis示例@Table(name = "t_user")public class UserPO {    @Id    private Long userId;    private String userName;    // 其他字段...} AI写代码java运行2、BO(Business Object,业务对象)定义:BO封装业务逻辑,可由多个PO组合而成,包含复杂的业务操作(如数据校验、流程控制)特点:独立于具体存储方式,可操作数据库、缓存、外部接口等例如,订单BO可能包含用户PO、商品PO和支付信息PO示例场景public class OrderBO {    private OrderPO order;    private List<ItemPO> items;    private UserPO user;    public BigDecimal calculateTotal() {        // 复杂的计算逻辑...    }}AI写代码java运行3、DTO(Data Transfer Object,数据传输对象)定义:DTO用于不同层之间的数据传输,尤其是Service层与Controller层的交互。它可以根据需求封装部分字段,减少不必要的数据传输特点:可能包含多个PO的组合或裁剪后的字段(例如从30个字段中选取10个传输)支持序列化,常用于远程调用(如RPC、HTTP接口)无业务逻辑典型场景public class UserDTO {    private String displayName;    private LocalDateTime registerTime;    // 转换方法    public static UserDTO fromPO(UserPO po) {        // 转换逻辑...    }}AI写代码java运行4、VO(View Object,视图对象)定义:VO是展示层(前端页面)直接使用的对象,仅包含前端需要展示的数据,通常以JSON形式返回应用场景:Controller层将数据封装为VO后传递给前端,避免暴露敏感字段(如密码、内部状态)示例public class UserVO {    private String formattedDate;    private String userLevel;    // 可能包含组合字段...}AI写代码java运行5、POJO(Plain Ordinary Java Object,简单Java对象)定义:POJO是所有简单Java对象的统称,VO、DTO、PO等均属于POJO特点:仅包含属性及Getter/Setter方法,不依赖特定框架二、对比与区别1、表格对比对象    应用场景    特点PO    数据库交互    与数据库表严格对应BO    Service层内部业务逻辑    封装复杂业务逻辑,可包含多个PO的组合DTO    Service层与Controller层间    聚合业务所需数据,可能组合多个POVO    Controller层与前端交互    按前端需求定制字段2、关键区别PO vs DTO:PO严格映射数据库表,DTO可根据业务需求裁剪字段DTO vs VO:DTO关注传输效率,VO关注展示效果。例如,DTO可能包含敏感字段(如用户ID),而VO仅展示脱敏后的信息BO vs PO:BO包含业务逻辑,PO仅存储数据。例如,订单BO可能计算总价,而订单PO仅记录金额3、流转图数据库DAO层Service层Controller层前端/客户端POBODTOVO查询用户信息并返回给前端DAO层通过UserDAO查询数据库,返回UserPOService层将UserPO转换为UserDTO,过滤敏感字段Controller层将UserDTO转换为UserVO,添加前端需要的格式化字段(如日期字符串)总结  合理使用VO、DTO、PO和BO等对象能有效实现解耦、提高灵活性和安全性。VO保护敏感数据,DTO适配不同接口需求,PO确保数据持久化准确,BO封装复杂业务逻辑。在开发中,根据项目复杂度选择合适的对象类型,并统一团队规范,提升代码可读性和可维护性。————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/qq_35512802/article/details/147122534
  • [技术干货] Java大模型开发指南
    一、Java大模型开发概述1.1 什么是大模型大模型(Large Language Model,LLM)是一种基于深度学习的自然语言处理模型,通过海量文本数据训练而成,能够理解和生成人类语言。大模型具有强大的语言理解、生成和推理能力,可以应用于多种场景,如对话系统、内容创作、代码生成等。大模型的发展经历了从BERT、GPT到ChatGPT、Claude等几个重要阶段,模型规模从最初的几亿参数增长到现在的数万亿参数,能力不断增强。1.2 Java在大模型开发中的角色Java作为一门成熟的编程语言,在大模型开发中扮演着重要角色:后端服务开发:构建稳定、高性能的大模型服务API模型部署与集成:将大模型集成到现有Java应用中数据处理与预处理:处理训练数据和用户输入性能优化:优化模型推理速度和资源消耗安全与权限控制:确保模型访问的安全性企业级应用集成:将大模型能力集成到企业级应用中1.3 大模型开发的关键挑战计算资源需求:大模型推理需要大量计算资源响应时间:需要优化推理速度以满足实时应用需求成本控制:API调用和模型部署成本较高安全与隐私:需要保护用户数据和模型安全质量与可靠性:确保模型输出的质量和可靠性可扩展性:支持高并发和水平扩展二、Java大模型开发技术栈2.1 核心框架与库Spring Boot/Spring Cloud:构建微服务架构DJL (Deep Java Library):Java深度学习框架Hugging Face Transformers:通过Java绑定使用预训练模型Apache OpenNLP:自然语言处理工具包Stanford CoreNLP:高级NLP功能Apache Lucene:文本索引和搜索Elasticsearch:分布式搜索引擎LangChain4j:Java版LangChain,用于构建基于大模型的应用Spring AI:Spring生态系统中的AI集成框架Apache Commons Lang:常用工具类库2.2 模型服务与部署Spring AI:Spring生态系统中的AI集成框架TensorFlow Serving:模型服务部署ONNX Runtime:跨平台推理引擎Docker/Kubernetes:容器化部署Spring Native:原生镜像支持Apache OpenWhisk:无服务器平台Spring Cloud Function:函数即服务Vert.x:响应式应用框架Quarkus:Kubernetes原生Java框架Micronaut:现代化JVM框架2.3 数据处理与存储Apache Spark:大规模数据处理Spring Data:数据访问层MongoDB/Elasticsearch:非关系型数据存储Redis:缓存和会话管理Apache Kafka:消息队列和流处理Apache Flink:流处理框架Apache Beam:批处理和流处理统一模型Spring Batch:批处理框架Apache NiFi:数据流自动化Apache Airflow:工作流编排2.4 监控与运维Spring Actuator:应用监控Micrometer:度量收集Prometheus:监控系统Grafana:可视化面板ELK Stack:日志分析Jaeger/Zipkin:分布式追踪Spring Cloud Sleuth:分布式追踪集成Spring Admin:管理界面Spring Boot Admin:应用管理Spring Cloud Config:配置管理三、Java大模型开发流程3.1 需求分析与设计确定应用场景:明确大模型在应用中的具体用途功能规划:定义API接口和功能模块架构设计:设计系统架构,包括模型服务、API网关、数据处理等性能需求:确定响应时间、并发量等性能指标安全需求:确定安全要求和隐私保护措施可扩展性需求:确定系统的扩展性和可维护性要求成本预算:评估开发和运营成本风险评估:识别潜在风险并制定应对策略3.2 模型选择与集成模型评估:根据需求选择合适的预训练模型模型适配:将模型转换为Java可用的格式模型优化:针对特定场景进行微调或优化模型封装:将模型封装为Java服务模型测试:评估模型性能和准确性模型版本管理:建立模型版本管理机制模型监控:实现模型性能和质量监控模型更新策略:制定模型更新和迭代策略3.3 后端服务开发API设计:设计RESTful API或gRPC服务服务实现:使用Spring Boot实现服务逻辑数据处理:实现数据预处理和后处理逻辑错误处理:实现健壮的错误处理机制日志记录:实现详细的日志记录性能优化:优化服务性能安全实现:实现安全措施测试:编写单元测试和集成测试3.4 部署与运维容器化:使用Docker容器化应用编排:使用Kubernetes进行服务编排监控:实现服务监控和日志收集扩展:设计水平扩展策略备份与恢复:实现数据备份和恢复机制灾难恢复:制定灾难恢复计划CI/CD:实现持续集成和持续部署性能测试:进行负载测试和性能测试四、Java大模型开发核心技术4.1 Spring AI集成Spring AI是Spring生态系统中的AI集成框架,简化了Java应用中集成大模型的过程:// 配置Spring AI@Configurationpublic class AiConfig {    @Bean    public OpenAiApi openAiApi() {        return OpenAiApi.builder()                .apiKey(System.getenv("OPENAI_API_KEY"))                .build();    }        @Bean    public AiClient aiClient(OpenAiApi openAiApi) {        return new OpenAiAiClient(openAiApi);    }}// 使用Spring AI@Servicepublic class ChatService {    private final AiClient aiClient;        public ChatService(AiClient aiClient) {        this.aiClient = aiClient;    }        public String generateResponse(String prompt) {        return aiClient.generate(prompt);    }        // 流式响应    public void generateStream(String prompt, Consumer<String> chunkConsumer) {        aiClient.generateStream(prompt, chunkConsumer);    }        // 带参数的生成    public String generateWithParams(String prompt, Map<String, Object> parameters) {        return aiClient.generate(prompt, parameters);    }}4.2 DJL模型加载与推理DJL (Deep Java Library) 是Java深度学习框架,支持加载和推理各种深度学习模型:// 使用DJL加载和推理模型public class ModelService {    private Predictor<String, String> predictor;        public void loadModel() {        // 加载模型        Criteria<String, String> criteria = Criteria.builder()                .setTypes(String.class, String.class)                .optModelPath(Paths.get("model-path"))                .optEngine("PyTorch")                .optProgress(new ProgressBar())                .build();                predictor = ModelZoo.loadModel(criteria);    }        public String predict(String input) {        return predictor.predict(input);    }        // 批量预测    public List<String> batchPredict(List<String> inputs) {        return predictor.batchPredict(inputs);    }        // 带参数的预测    public String predictWithParams(String input, Map<String, Object> parameters) {        return predictor.predict(input, parameters);    }}4.3 异步处理与响应式编程大模型推理通常耗时较长,使用异步处理和响应式编程可以提高系统吞吐量:// 使用Spring WebFlux进行响应式编程@RestController@RequestMapping("/api/chat")public class ChatController {    private final ChatService chatService;        public ChatController(ChatService chatService) {        this.chatService = chatService;    }        @PostMapping    public Mono<ChatResponse> chat(@RequestBody ChatRequest request) {        return Mono.fromCallable(() -> chatService.generateResponse(request.getPrompt()))                .subscribeOn(Schedulers.boundedElastic())                .map(response -> new ChatResponse(response));    }        // 带超时的请求    @PostMapping("/timeout")    public Mono<ChatResponse> chatWithTimeout(@RequestBody ChatRequest request) {        return Mono.fromCallable(() -> chatService.generateResponse(request.getPrompt()))                .subscribeOn(Schedulers.boundedElastic())                .timeout(Duration.ofSeconds(30))                .onErrorResume(TimeoutException.class, e ->                     Mono.just(new ChatResponse("请求超时,请稍后重试")))                .map(response -> new ChatResponse(response));    }        // 带重试的请求    @PostMapping("/retry")    public Mono<ChatResponse> chatWithRetry(@RequestBody ChatRequest request) {        return Mono.fromCallable(() -> chatService.generateResponse(request.getPrompt()))                .subscribeOn(Schedulers.boundedElastic())                .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))                .map(response -> new ChatResponse(response));    }}4.4 流式响应处理对于长文本生成,流式响应可以提供更好的用户体验:// 使用Spring WebFlux实现流式响应@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<ServerSentEvent<String>> streamChat(@RequestParam String prompt) {    return Flux.create(sink -> {        chatService.generateStream(prompt, chunk -> {            sink.next(ServerSentEvent.<String>builder()                    .data(chunk)                    .build());        });        sink.complete();    });}// 带错误处理的流式响应@GetMapping(value = "/stream-safe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<ServerSentEvent<String>> streamChatSafe(@RequestParam String prompt) {    return Flux.<ServerSentEvent<String>>create(sink -> {        try {            chatService.generateStream(prompt, chunk -> {                sink.next(ServerSentEvent.<String>builder()                        .data(chunk)                        .build());            });            sink.complete();        } catch (Exception e) {            sink.error(e);        }    })    .onErrorResume(e -> Flux.just(        ServerSentEvent.<String>builder()            .data("发生错误: " + e.getMessage())            .build()    ));}4.5 缓存与性能优化使用缓存可以减少重复计算,提高系统性能:// 使用Spring Cache进行缓存@Servicepublic class CachedModelService {    private final ModelService modelService;        public CachedModelService(ModelService modelService) {        this.modelService = modelService;    }        @Cacheable(value = "modelResponses", key = "#input")    public String getCachedResponse(String input) {        return modelService.predict(input);    }        // 带TTL的缓存    @Cacheable(value = "modelResponses", key = "#input", unless = "#result == null")    public String getCachedResponseWithTTL(String input) {        return modelService.predict(input);    }        // 缓存预热    @PostConstruct    public void warmupCache() {        List<String> commonInputs = Arrays.asList(            "常见问题1", "常见问题2", "常见问题3"        );                commonInputs.forEach(this::getCachedResponse);    }}4.6 模型量化与优化模型量化可以减小模型体积,提高推理速度:// 使用DJL进行模型量化public class QuantizedModelService {    private Predictor<String, String> predictor;        public void loadQuantizedModel() {        // 加载量化模型        Criteria<String, String> criteria = Criteria.builder()                .setTypes(String.class, String.class)                .optModelPath(Paths.get("quantized-model-path"))                .optEngine("PyTorch")                .optProgress(new ProgressBar())                .build();                predictor = ModelZoo.loadModel(criteria);    }        public String predict(String input) {        return predictor.predict(input);    }}4.7 多模型集成与路由集成多个模型并根据需求路由请求:// 多模型集成与路由@Servicepublic class MultiModelService {    private final Map<String, ModelService> modelServices;        public MultiModelService(List<ModelService> services) {        modelServices = services.stream()                .collect(Collectors.toMap(ModelService::getModelType, Function.identity()));    }        public String routeRequest(String input, String modelType) {        ModelService service = modelServices.get(modelType);        if (service == null) {            throw new IllegalArgumentException("不支持的模型类型: " + modelType);        }        return service.predict(input);    }        // 智能路由    public String smartRoute(String input) {        // 根据输入内容选择合适的模型        String modelType = selectModelType(input);        return routeRequest(input, modelType);    }        private String selectModelType(String input) {        // 实现模型选择逻辑        if (input.contains("代码")) {            return "code";        } else if (input.contains("图像")) {            return "vision";        } else {            return "general";        }    }}五、Java大模型应用场景5.1 智能客服系统构建基于大模型的智能客服系统,提供24/7自动回答服务:@Servicepublic class CustomerServiceBot {    private final AiClient aiClient;    private final ConversationRepository conversationRepository;        public CustomerServiceBot(AiClient aiClient, ConversationRepository conversationRepository) {        this.aiClient = aiClient;        this.conversationRepository = conversationRepository;    }        public String handleCustomerQuery(String customerId, String query) {        // 获取历史对话        List<Message> history = conversationRepository.findByCustomerId(customerId);                // 构建提示词        String prompt = buildPromptWithHistory(history, query);                // 生成回复        String response = aiClient.generate(prompt);                // 保存对话历史        conversationRepository.save(new Message(customerId, query, response));                return response;    }        private String buildPromptWithHistory(List<Message> history, String query) {        StringBuilder prompt = new StringBuilder();        prompt.append("你是一个专业的客服代表,请根据以下对话历史回答用户的问题:\n\n");                // 添加历史对话        for (Message message : history) {            prompt.append("用户: ").append(message.getQuery()).append("\n");            prompt.append("客服: ").append(message.getResponse()).append("\n\n");        }                // 添加当前问题        prompt.append("用户: ").append(query).append("\n");        prompt.append("客服: ");                return prompt.toString();    }        // 带情感分析的客服    public String handleCustomerQueryWithSentiment(String customerId, String query) {        // 分析用户情感        String sentiment = analyzeSentiment(query);                // 根据情感调整回复风格        String prompt = buildPromptWithSentiment(history, query, sentiment);                // 生成回复        String response = aiClient.generate(prompt);                // 保存对话历史        conversationRepository.save(new Message(customerId, query, response, sentiment));                return response;    }        private String analyzeSentiment(String text) {        // 实现情感分析逻辑        return "positive"; // 或 "negative", "neutral"    }}5.2 代码生成与辅助使用大模型辅助代码生成和重构:@Servicepublic class CodeAssistant {    private final AiClient aiClient;        public CodeAssistant(AiClient aiClient) {        this.aiClient = aiClient;    }        public String generateCode(String description, String language) {        String prompt = String.format("Generate %s code for: %s", language, description);        return aiClient.generate(prompt);    }        public String refactorCode(String code, String instructions) {        String prompt = String.format("Refactor the following code according to these instructions: %s\n\nCode:\n%s",                 instructions, code);        return aiClient.generate(prompt);    }        // 代码解释    public String explainCode(String code) {        String prompt = String.format("Explain the following code in detail:\n\n%s", code);        return aiClient.generate(prompt);    }        // 代码优化    public String optimizeCode(String code) {        String prompt = String.format("Optimize the following code for performance and readability:\n\n%s", code);        return aiClient.generate(prompt);    }        // 单元测试生成    public String generateUnitTests(String code, String language) {        String prompt = String.format("Generate unit tests for the following %s code:\n\n%s", language, code);        return aiClient.generate(prompt);    }}5.3 内容生成与摘要使用大模型生成和摘要内容:@Servicepublic class ContentGenerator {    private final AiClient aiClient;        public ContentGenerator(AiClient aiClient) {        this.aiClient = aiClient;    }        public String generateArticle(String topic, String style) {        String prompt = String.format("Write an article about %s in %s style", topic, style);        return aiClient.generate(prompt);    }        public String summarizeText(String text, int maxLength) {        String prompt = String.format("Summarize the following text in %d words or less:\n\n%s", maxLength, text);        return aiClient.generate(prompt);    }        // 内容分类    public String classifyContent(String content) {        String prompt = String.format("Classify the following content into one of these categories: News, Opinion, Technical, Entertainment, Other\n\n%s", content);        return aiClient.generate(prompt);    }        // 关键词提取    public List<String> extractKeywords(String content) {        String prompt = String.format("Extract the top 5 keywords from the following text:\n\n%s", content);        String response = aiClient.generate(prompt);        return Arrays.asList(response.split(","));    }        // 内容翻译    public String translateContent(String content, String targetLanguage) {        String prompt = String.format("Translate the following text to %s:\n\n%s", targetLanguage, content);        return aiClient.generate(prompt);    }}5.4 多模态应用结合图像和文本的多模态应用:@Servicepublic class ImageCaptionService {    private final AiClient aiClient;    private final ImageProcessor imageProcessor;        public ImageCaptionService(AiClient aiClient, ImageProcessor imageProcessor) {        this.aiClient = aiClient;        this.imageProcessor = imageProcessor;    }        public String generateImageCaption(byte[] imageData) {        // 处理图像        String imageDescription = imageProcessor.processImage(imageData);                // 生成描述        String prompt = "Generate a detailed caption for this image: " + imageDescription;        return aiClient.generate(prompt);    }        // 图像分类    public List<String> classifyImage(byte[] imageData) {        // 处理图像        String imageDescription = imageProcessor.processImage(imageData);                // 分类        String prompt = "Classify this image into categories: " + imageDescription;        String response = aiClient.generate(prompt);        return Arrays.asList(response.split(","));    }        // 图像问答    public String answerImageQuestion(byte[] imageData, String question) {        // 处理图像        String imageDescription = imageProcessor.processImage(imageData);                // 回答问题        String prompt = String.format("Based on this image: %s\n\nAnswer the following question: %s",                 imageDescription, question);        return aiClient.generate(prompt);    }}5.5 数据分析与洞察使用大模型进行数据分析和洞察:@Servicepublic class DataAnalyticsService {    private final AiClient aiClient;        public DataAnalyticsService(AiClient aiClient) {        this.aiClient = aiClient;    }        public String analyzeData(String data, String format) {        String prompt = String.format("Analyze the following %s data and provide insights:\n\n%s", format, data);        return aiClient.generate(prompt);    }        public String generateDataVisualization(String data, String chartType) {        String prompt = String.format("Suggest a %s visualization for the following data:\n\n%s", chartType, data);        return aiClient.generate(prompt);    }        public String detectAnomalies(String timeSeriesData) {        String prompt = String.format("Detect anomalies in the following time series data:\n\n%s", timeSeriesData);        return aiClient.generate(prompt);    }        public String forecastTrends(String historicalData, String metric) {        String prompt = String.format("Forecast future trends for %s based on the following historical data:\n\n%s",                 metric, historicalData);        return aiClient.generate(prompt);    }}六、Java大模型开发最佳实践6.1 模型选择与优化选择合适的模型:根据应用场景选择合适大小的模型模型量化:使用量化技术减小模型体积,提高推理速度模型剪枝:移除不重要的权重,减小模型复杂度批处理优化:使用批处理提高吞吐量模型蒸馏:使用知识蒸馏技术将大模型压缩为小模型模型缓存:缓存常用查询结果模型版本管理:建立模型版本管理机制A/B测试:对不同模型版本进行A/B测试6.2 系统架构设计微服务架构:将模型服务拆分为独立微服务API网关:使用API网关管理服务访问负载均衡:实现模型服务的负载均衡服务发现:使用服务发现机制管理服务实例熔断与降级:实现服务熔断和降级机制异步处理:使用异步处理提高系统吞吐量事件驱动:采用事件驱动架构处理模型请求无服务器架构:考虑使用无服务器架构降低成本6.3 安全与隐私访问控制:实现基于角色的访问控制数据加密:加密敏感数据和模型输入验证:验证和清理用户输入审计日志:记录模型使用情况数据脱敏:对敏感数据进行脱敏处理安全传输:使用HTTPS等安全传输协议模型保护:保护模型知识产权合规性:确保符合数据保护法规6.4 监控与可观测性性能监控:监控模型推理性能资源使用:监控CPU、内存和GPU使用情况错误跟踪:跟踪和记录错误用户反馈:收集用户反馈以改进模型日志聚合:聚合和分析日志数据告警机制:设置性能和质量告警仪表盘:创建监控仪表盘追踪:实现分布式追踪6.5 成本优化按需扩展:根据负载自动扩展资源资源调度:优化资源调度策略缓存策略:实现多级缓存批处理:使用批处理减少API调用模型压缩:压缩模型减小资源消耗冷启动优化:优化模型冷启动时间资源预留:为关键服务预留资源成本监控:监控和分析成本七、Java大模型开发工具与资源7.1 开发工具IntelliJ IDEA:Java IDE,支持Spring开发Spring Tool Suite:基于Eclipse的Spring开发工具Maven/Gradle:依赖管理和构建工具Docker Desktop:容器化开发环境Postman:API测试工具Visual Studio Code:轻量级编辑器,支持Java扩展Jupyter Notebook:交互式开发环境Git:版本控制工具Jenkins/GitLab CI:持续集成工具Kubernetes Dashboard:Kubernetes管理界面7.2 学习资源Spring AI文档:https://docs.spring.io/spring-ai/reference/DJL文档:https://djl.ai/docs/Hugging Face Java API:https://huggingface.co/docs/api-inference/javaSpring Boot文档:https://spring.io/projects/spring-bootKubernetes文档:https://kubernetes.io/docs/LangChain4j文档:https://github.com/langchain4j/langchain4jJava深度学习教程:https://www.deeplearning4j.org/tutorialsSpring Cloud文档:https://spring.io/projects/spring-cloud响应式编程指南:https://projectreactor.io/docs/core/release/reference/Java并发编程:https://docs.oracle.com/javase/tutorial/essential/concurrency/7.3 社区与支持Spring社区:https://spring.io/communityDJL社区:https://djl.ai/communityHugging Face社区:https://huggingface.co/communityStack Overflow:Java和Spring相关问答GitHub:开源项目和示例代码Reddit r/java:Java相关讨论Java User Groups:本地Java用户组Spring论坛:Spring相关问题讨论AI开发者社区:AI开发相关讨论技术博客:Java和AI相关博客7.4 示例项目Spring AI示例:https://github.com/spring-projects/spring-ai/tree/main/spring-ai-samplesDJL示例:https://github.com/deepjavalibrary/djl/tree/master/examplesLangChain4j示例:https://github.com/langchain4j/langchain4j/tree/master/langchain4j-examplesSpring Boot AI示例:https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-samplesKubernetes AI部署示例:https://github.com/kubeflow/kubeflow/tree/master/components/example-notebook-servers八、Java大模型开发未来趋势8.1 技术发展趋势更高效的模型部署:更轻量级的模型和更高效的推理引擎多模态融合:更强大的多模态模型支持边缘计算:在边缘设备上部署大模型联邦学习:分布式模型训练和更新自动机器学习:自动化的模型选择和优化量子计算集成:量子计算与大模型的结合神经符号集成:神经网络与符号推理的结合自监督学习:减少对标注数据的依赖可解释性增强:提高模型决策的可解释性低资源学习:在有限资源下训练和部署模型8.2 应用发展趋势个性化AI助手:更个性化的AI助手和代理行业特定模型:针对特定行业优化的模型低代码/无代码集成:简化大模型集成过程增强现实集成:与AR/VR技术的结合自主系统:更自主的AI系统创意内容生成:更高质量的创意内容生成科学发现辅助:辅助科学研究和发现医疗诊断支持:辅助医疗诊断和治疗教育个性化:个性化学习和教育可持续发展应用:应用于环境和社会可持续发展九、Java大模型开发实战案例9.1 智能客服系统案例项目背景:某电商平台需要构建智能客服系统,处理大量用户咨询。技术方案:使用Spring Boot构建后端服务集成OpenAI API进行对话生成使用Redis缓存常见问题回答使用MongoDB存储对话历史使用Spring Security实现访问控制关键代码:@RestController@RequestMapping("/api/customer-service")public class CustomerServiceController {    private final CustomerServiceBot bot;        @PostMapping("/query")    public ResponseEntity<String> handleQuery(@RequestBody QueryRequest request) {        String response = bot.handleCustomerQuery(request.getCustomerId(), request.getQuery());        return ResponseEntity.ok(response);    }        @GetMapping("/history/{customerId}")    public ResponseEntity<List<Message>> getHistory(@PathVariable String customerId) {        List<Message> history = conversationRepository.findByCustomerId(customerId);        return ResponseEntity.ok(history);    }}实现效果:系统能够处理80%的常见问题响应时间平均在2秒以内用户满意度提升30%9.2 代码生成助手案例项目背景:某软件开发公司需要提高开发效率,构建代码生成助手。技术方案:使用Spring Boot构建API服务集成Codex API进行代码生成使用Spring Cache缓存常用代码片段使用GitHub API集成版本控制使用Docker容器化部署关键代码:@Servicepublic class CodeGenerationService {    private final AiClient aiClient;    private final CodeRepository codeRepository;        public CodeSnippet generateCodeSnippet(String description, String language) {        // 检查缓存        String cacheKey = language + ":" + description;        CodeSnippet cached = codeCache.get(cacheKey);        if (cached != null) {            return cached;        }                // 生成代码        String code = aiClient.generate("Generate " + language + " code for: " + description);                // 创建代码片段        CodeSnippet snippet = new CodeSnippet(description, code, language);                // 保存到缓存和数据库        codeCache.put(cacheKey, snippet);        codeRepository.save(snippet);                return snippet;    }}实现效果:开发效率提升25%代码质量一致性提高新员工上手速度加快9.3 内容生成平台案例项目背景:某内容平台需要自动生成高质量文章,减少人工创作成本。技术方案:使用Spring Cloud构建微服务架构集成GPT-4 API进行内容生成使用Elasticsearch进行内容索引和搜索使用Kafka进行事件驱动处理使用Kubernetes进行容器编排关键代码:@Servicepublic class ContentGenerationService {    private final AiClient aiClient;    private final ContentRepository contentRepository;    private final KafkaTemplate<String, ContentEvent> kafkaTemplate;        public Content generateContent(ContentRequest request) {        // 生成内容        String content = aiClient.generate(buildPrompt(request));                // 创建内容对象        Content contentObj = new Content(            request.getTitle(),            content,            request.getCategory(),            request.getAuthor()        );                // 保存到数据库        contentRepository.save(contentObj);                // 发送事件        kafkaTemplate.send("content-events", new ContentEvent(contentObj, "CREATED"));                return contentObj;    }        private String buildPrompt(ContentRequest request) {        return String.format(            "Write a %s article about %s with title '%s'. " +            "The article should be informative, engaging, and approximately %d words long.",            request.getStyle(),            request.getTopic(),            request.getTitle(),            request.getWordCount()        );    }}实现效果:内容生成成本降低60%内容产量增加300%内容质量评分保持在4.2/5以上十、常见问题与解决方案10.1 性能问题问题:大模型推理速度慢,影响用户体验。解决方案:使用模型量化和剪枝减小模型体积实现多级缓存减少重复计算使用异步处理和流式响应采用批处理提高吞吐量使用GPU加速推理过程10.2 成本问题问题:API调用成本高,难以控制预算。解决方案:实现请求限流和配额管理使用缓存减少API调用采用混合模型策略,简单问题使用小模型实现成本监控和告警优化提示词减少token使用10.3 质量问题问题:模型输出质量不稳定,有时产生错误或无关内容。解决方案:实现输出验证和过滤机制使用更高质量的提示词工程实现人工审核流程收集用户反馈持续改进使用特定领域微调模型10.4 安全与隐私问题问题:模型可能泄露敏感信息或产生有害内容。解决方案:实现输入和输出过滤使用内容安全API检测有害内容实现数据脱敏和匿名化建立安全审计和监控制定明确的使用政策10.5 可扩展性问题问题:系统难以应对流量增长和需求变化。解决方案:采用微服务架构提高灵活性实现自动扩缩容使用负载均衡分散流量采用事件驱动架构解耦服务实现服务降级和熔断机制十一、总结Java大模型开发是一个快速发展的领域,结合Java的稳定性和大模型的强大能力,可以构建各种智能应用。通过掌握Spring AI、DJL等工具,以及异步处理、流式响应等技术,开发者可以高效地构建和部署大模型应用。随着技术的不断发展,Java大模型开发将变得更加简单和高效,为开发者提供更多可能性。通过遵循最佳实践,关注安全与性能,开发者可以构建出高质量的大模型应用,满足各种业务需求————————————————原文链接:https://blog.csdn.net/weixin_74417835/article/details/147067060
  • [技术干货] Java-断点续传
    什么是断点续传?引用百度百科:对断点续传的定义个人理解:如果我们要在项目的媒资管理部分,上传视频文件(通常这类视频文件都比较大),http协议本身对上传文件大小没有限制,但是客户的网络环境质量,电脑硬件环境参差不齐,可能会导致一个大文件快上传完了出现断网的情况,从而导致文件没有上传成功,需要客户重新上传,用户体验非常差,所以对于大文件上传的要求是能做到 断点续传断点续传流程图如下图:简要概述实现步骤:前端上传完先把文件分成块一块一块的上传,上传中断后重新上传,已上传的分块则不用再上传各分块上传完成最后在服务端合并文件(为了方便理解,我用Java代码的方法 测试文件的分块与合并)先进行大文件资源 分块操作:1.导入一系列的包package com.xuecheng.media; import org.apache.commons.codec.digest.DigestUtils;import org.apache.commons.io.IOUtils;import org.junit.jupiter.api.Test; import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.RandomAccessFile;import java.util.*;导入多个外部和Java自带的库,其中org.apache.commons.codec.digest.DigestUtils:用于生成文章的摘要,能够在断点续传中验证文件的完整性2.定义相关的类和方法public class BigFileTest {    //测试文件分块方法    @Test    public void testChunk() throws IOException {定义一个公共类BigFileTest,用于包含文件处理相关的测试方法,并且定义了一个testChunk方法,该方法用于执行文件分块操作,并声明了可能抛出IOException异常,因为文件操作可能会失败3.创建分块文件File sourceFile = new File("d:/develop/bigfile_test/nacos.mp4");String chunkPath = "d:/develop/bigfile_test/chunk/";File chunkFolder = new File(chunkPath);if (!chunkFolder.exists()) {    chunkFolder.mkdirs();}创建一个File对象,表示要对d:/develop/bigfile_test/路径下的nacos.mp4源文件进行分块,并创建一个File对象,表示分块存储的文件夹,如果分块文件夹不存在,则创建它 4.对大文件分成文件块//分块大小long chunkSize = 1024 * 1024 * 1;//分块数量long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize);System.out.println("分块总数:" + chunkNum);然后对 文件进行分割,定义每个分块的大小,通过源文件的长度除以分块大小(要采用向上取整噢)使用Math.ceil方法向上取整,计算出分块的总数5.进行对文件的读取写入//缓冲区大小byte[] b = new byte[1024];//使用RandomAccessFile访问文件RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r");//分块for (int i = 0; i < chunkNum; i++) {    //创建分块文件    File file = new File(chunkPath + i);    if (file.exists()) {        file.delete();    }    boolean newFile = file.createNewFile();    if (newFile) {        //向分块文件中写数据        RandomAccessFile raf_write = new RandomAccessFile(file, "rw");        int len = -1;        while ((len = raf_read.read(b))!= -1) {            raf_write.write(b, 0, len);            if (file.length() >= chunkSize) {                break;            }        }        raf_write.close();        System.out.println("完成分块" + i);    }}raf_read.close();然后进行缓冲区与文件读取写入,以上代码块创建了一个大小为1024字节的缓冲区数组b,用于读取源文件数据,并且以只读模式打开源文件,然后进行分块操作循环分块数量chunkNum(对源文件进行分块后得到的分块总数),if循环判断分块文件是否创建成功,如果创建成功,则从源文件中读取数据到缓冲区,直到读取到文件末尾再进行分块文件 合并操作:各个文件块读取完毕后,就可以进行 文件合并 操作,分块合并步骤File chunkFolder = new File("D:\\...\\chunk\\");  // 分块文件目录File sourceFile = new File("D:\\...\\星际牛仔1.mp4");  // 原始文件File mergeFile = new File("D:\\...\\星际牛仔1-1.mp4");  // 合并后的文件mergeFile.createNewFile();  // 创建空的目标文件定义分块文件的存储目录,原始文件路径和合并后的文件路径,并创建一个空的目标文件RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");  // 以读写模式打开合并文件byte[] buffer = new byte[1024];  // 读写缓冲区(1KB)初始化写入流,RandomAccessFile是一个用于随机访问文件的类,适合大文件操作,“rw”表示读写模式,byte[ ] buffer = new byte[1024]这段代码创建了一个字节数组,作为读写缓冲区(临时存储从文件或网络中读取的数据),每次读取1KB数据,减少磁盘I/O次数//listFiles()表示返回目录中所有文件和子目录的File数组,files是包含所有分块文件的数组File[] files = chunkFolder.listFiles();//将数组转换为list集合,fileList是包含所以分块文件的list<File>List<File> fileList = Arrays.asList(files);//然后对List进行排序,Comparator.comparingInt创建一个比较器,按照指定规则排序(将文件名转换为整数进行排序)Collections.sort(fileList, Comparator.comparingInt(o -> Integer.parseInt(o.getName())));对分块文件进行排序,o -> Integer.parseInt(o.getName()):是一个Lambda表达式,用于将文件名转换为整数进行排序//循环遍历分块文件列表for (File chunkFile : fileList) {//以只读模式打开分块文件 "r"表示以只读模式打开    RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "r"); //读取分块文件内容并写入合并文件    int len;    while ((len = raf_read.read(buffer)) != -1) {          raf_write.write(buffer, 0, len);      }    raf_read.close();  // 关闭分块文件流}raf_write.close();  // 关闭合并文件流这段代码遍历分块文件列表,并以只读模式打开分块文件,然后读取分块文件内容并写入到合并文件,最后关闭分块文件流和合并文件流这里我再总结一下掌握断点续传的技术,我们能够将多个分块文件合并成一个完整文件,避免用户在下载过程因为网络,系统等一系列原因导致下载中断,而需重新开始下载的情况。而是可以直接从中断处继续下载,节省了我们时间和网络资源————————————————原文链接:https://blog.csdn.net/2301_79757798/article/details/145633400
  • [技术干货] java内部类赋值_详解 Java 内部类
    内部类在 Java 里面算是非常常见的一个功能了,在日常开发中我们肯定多多少少都用过,这里总结一下关于 Java 中内部类的相关知识点和一些使用内部类时需要注意的点。从种类上说,内部类可以分为四类:普通内部类、静态内部类、匿名内部类、局部内部类。我们来一个个看:普通内部类这个是最常见的内部类之一了,其定义也很简单,在一个类里面作为类的一个字段直接定义就可以了,例:public class InnerClassTest {public class InnerClassA {}}在这里 InnerClassA 类为 InnerClassTest 类的普通内部类,在这种定义方式下,普通内部类对象依赖外部类对象而存在,即在创建一个普通内部类对象时首先需要创建其外部类对象,我们在创建上面代码中的 InnerClassA 对象时先要创建 InnerClassTest 对象,例:public class InnerClassTest {public int field1 = 1;protected int field2 = 2;int field3 = 3;private int field4 = 4;public InnerClassTest() {// 在外部类对象内部,直接通过 new InnerClass(); 创建内部类对象InnerClassA innerObj = new InnerClassA();System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");System.out.println("其内部类的 field1 字段的值为: " + innerObj.field1);System.out.println("其内部类的 field2 字段的值为: " + innerObj.field2);System.out.println("其内部类的 field3 字段的值为: " + innerObj.field3);System.out.println("其内部类的 field4 字段的值为: " + innerObj.field4);}public class InnerClassA {public int field1 = 1;protected int field2 = 2;int field3 = 3;private int field4 = 4;//        static int field5 = 5; // 编译错误!普通内部类中不能定义 static 属性public InnerClassA() {System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");System.out.println("其外部类的 field1 字段的值为: " + field1);System.out.println("其外部类的 field2 字段的值为: " + field2);System.out.println("其外部类的 field3 字段的值为: " + field3);System.out.println("其外部类的 field4 字段的值为: " + field4);}}public static void main(String[] args) {InnerClassTest outerObj = new InnerClassTest();// 不在外部类内部,使用:外部类对象. new 内部类构造器(); 的方式创建内部类对象//        InnerClassA innerObj = outerObj.new InnerClassA();}}这里的内部类就像外部类声明的一个属性字段一样,因此其的对象时依附于外部类对象而存在的,我们来看一下结果:我们注意到,内部类对象可以访问外部类对象中所有访问权限的字段,同时,外部类对象也可以通过内部类的对象引用来访问内部类中定义的所有访问权限的字段,后面我们将从源码里面分析具体的原因。我们下面来看一下静态内部类。静态内部类我们知道,一个类的静态成员独立于这个类的任何一个对象存在,只要在具有访问权限的地方,我们就可以通过 类名.静态成员名 的形式来访问这个静态成员,同样的,静态内部类也是作为一个外部类的静态成员而存在,创建一个类的静态内部类对象不需要依赖其外部类对象。例:public class InnerClassTest {public int field1 = 1;public InnerClassTest() {System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");// 创建静态内部类对象StaticClass innerObj = new StaticClass();System.out.println("其内部类的 field1 字段的值为: " + innerObj.field1);System.out.println("其内部类的 field2 字段的值为: " + innerObj.field2);System.out.println("其内部类的 field3 字段的值为: " + innerObj.field3);System.out.println("其内部类的 field4 字段的值为: " + innerObj.field4);}static class StaticClass {public int field1 = 1;protected int field2 = 2;int field3 = 3;private int field4 = 4;// 静态内部类中可以定义 static 属性static int field5 = 5;public StaticClass() {System.out.println("创建 " + StaticClass.class.getSimpleName() + " 对象");//            System.out.println("其外部类的 field1 字段的值为: " + field1); // 编译错误!!}}public static void main(String[] args) {// 无需依赖外部类对象,直接创建内部类对象//        InnerClassTest.StaticClass staticClassObj = new InnerClassTest.StaticClass();InnerClassTest outerObj = new InnerClassTest();}}结果:可以看到,静态内部类就像外部类的一个静态成员一样,创建其对象无需依赖外部类对象(访问一个类的静态成员也无需依赖这个类的对象,因为它是独立于所有类的对象的)。但是于此同时,静态内部类中也无法访问外部类的非静态成员,因为外部类的非静态成员是属于每一个外部类对象的,而本身静态内部类就是独立外部类对象存在的,所以静态内部类不能访问外部类的非静态成员,而外部类依然可以访问静态内部类对象的所有访问权限的成员,这一点和普通内部类无异。匿名内部类匿名内部类有多种形式,其中最常见的一种形式莫过于在方法参数中新建一个接口对象 / 类对象,并且实现这个接口声明 / 类中原有的方法了:public class InnerClassTest {public int field1 = 1;protected int field2 = 2;int field3 = 3;private int field4 = 4;public InnerClassTest() {System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");}// 自定义接口interface OnClickListener {void onClick(Object obj);}private void anonymousClassTest() {// 在这个过程中会新建一个匿名内部类对象,// 这个匿名内部类实现了 OnClickListener 接口并重写 onClick 方法OnClickListener clickListener = new OnClickListener() {// 可以在内部类中定义属性,但是只能在当前内部类中使用,// 无法在外部类中使用,因为外部类无法获取当前匿名内部类的类名,// 也就无法创建匿名内部类的对象int field = 1;@Overridepublic void onClick(Object obj) {System.out.println("对象 " + obj + " 被点击");System.out.println("其外部类的 field1 字段的值为: " + field1);System.out.println("其外部类的 field2 字段的值为: " + field2);System.out.println("其外部类的 field3 字段的值为: " + field3);System.out.println("其外部类的 field4 字段的值为: " + field4);}};// new Object() 过程会新建一个匿名内部类,继承于 Object 类,// 并重写了 toString() 方法clickListener.onClick(new Object() {@Overridepublic String toString() {return "obj1";}});}public static void main(String[] args) {InnerClassTest outObj = new InnerClassTest();outObj.anonymousClassTest();}}来看看结果:上面的代码中展示了常见的两种使用匿名内部类的情况:直接 new 一个接口,并实现这个接口声明的方法,在这个过程其实会创建一个匿名内部类实现这个接口,并重写接口声明的方法,然后再创建一个这个匿名内部类的对象并赋值给前面的 OnClickListener 类型的引用;new 一个已经存在的类 / 抽象类,并且选择性的实现这个类中的一个或者多个非 final 的方法,这个过程会创建一个匿名内部类对象继承对应的类 / 抽象类,并且重写对应的方法。同样的,在匿名内部类中可以使用外部类的属性,但是外部类却不能使用匿名内部类中定义的属性,因为是匿名内部类,因此在外部类中无法获取这个类的类名,也就无法得到属性信息。局部内部类局部内部类使用的比较少,其声明在一个方法体 / 一段代码块的内部,而且不在定义类的定义域之内便无法使用,其提供的功能使用匿名内部类都可以实现,而本身匿名内部类可以写得比它更简洁,因此局部内部类用的比较少。来看一个局部内部类的小例子:public class InnerClassTest {public int field1 = 1;protected int field2 = 2;int field3 = 3;private int field4 = 4;public InnerClassTest() {System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");}private void localInnerClassTest() {// 局部内部类 A,只能在当前方法中使用class A {// static int field = 1; // 编译错误!局部内部类中不能定义 static 字段public A() {System.out.println("创建 " + A.class.getSimpleName() + " 对象");System.out.println("其外部类的 field1 字段的值为: " + field1);System.out.println("其外部类的 field2 字段的值为: " + field2);System.out.println("其外部类的 field3 字段的值为: " + field3);System.out.println("其外部类的 field4 字段的值为: " + field4);}}A a = new A();if (true) {// 局部内部类 B,只能在当前代码块中使用class B {public B() {System.out.println("创建 " + B.class.getSimpleName() + " 对象");System.out.println("其外部类的 field1 字段的值为: " + field1);System.out.println("其外部类的 field2 字段的值为: " + field2);System.out.println("其外部类的 field3 字段的值为: " + field3);System.out.println("其外部类的 field4 字段的值为: " + field4);}}B b = new B();}//        B b1 = new B(); // 编译错误!不在类 B 的定义域内,找不到类 B,}public static void main(String[] args) {InnerClassTest outObj = new InnerClassTest();outObj.localInnerClassTest();}}同样的,在局部内部类里面可以访问外部类对象的所有访问权限的字段,而外部类却不能访问局部内部类中定义的字段,因为局部内部类的定义只在其特定的方法体 / 代码块中有效,一旦出了这个定义域,那么其定义就失效了,就像代码注释中描述的那样,即外部类不能获取局部内部类的对象,因而无法访问局部内部类的字段。最后看看运行结果:内部类的嵌套内部类的嵌套,即为内部类中再定义内部类,这个问题从内部类的分类角度去考虑比较合适:普通内部类:在这里我们可以把它看成一个外部类的普通成员方法,在其内部可以定义普通内部类(嵌套的普通内部类),但是无法定义 static 修饰的内部类,就像你无法在成员方法中定义 static 类型的变量一样,当然也可以定义匿名内部类和局部内部类;静态内部类:因为这个类独立于外部类对象而存在,我们完全可以将其拿出来,去掉修饰它的 static 关键字,他就是一个完整的类,因此在静态内部类内部可以定义普通内部类,也可以定义静态内部类,同时也可以定义 static 成员;匿名内部类:和普通内部类一样,定义的普通内部类只能在这个匿名内部类中使用,定义的局部内部类只能在对应定义域内使用;局部内部类:和匿名内部类一样,但是嵌套定义的内部类只能在对应定义域内使用。深入理解内部类不知道小伙伴们对上面的代码有没有产生疑惑:非静态内部类可以访问外部类所有访问权限修饰的字段(即包括了 private 权限的),同时,外部类也可以访问内部类的所有访问权限修饰的字段。而我们知道,private 权限的字段只能被当前类本身访问。然而在上面我们确实在代码中直接访问了对应外部类 / 内部类的 private 权限的字段,要解除这个疑惑,只能从编译出来的类下手了,为了简便,这里采用下面的代码进行测试:public class InnerClassTest {int field1 = 1;private int field2 = 2;public InnerClassTest() {InnerClassA inner = new InnerClassA();int v = inner.x2;}public class InnerClassA {int x1 = field1;private int x2 = field2;}}我在外部类中定义了一个默认访问权限(同一个包内的类可以访问)的字段 field1, 和一个 private 权限的字段 field2 ,并且定义了一个内部类 InnerClassA ,并且在这个内部类中也同样定义了两个和外部类中定义的相同修饰权限的字段,并且访问了外部类对应的字段。最后在外部类的构造方法中我定义了一个方法内变量赋值为内部类中 private 权限的字段。我们用 javac 命令(javac InnerClassTest.java)编译这个 .java 文件,会得到两个 .classs 文件。InnerClassTest.class 和 InnerClassTest$InnerClassA.class,我们再用 javap -c 命令(javap -c InnerClassTest 和 javap -c InnerClassTest$InnerClassA)分别反编译这两个 .class 文件,InnerClassTest.class 的字节码如下:我们注意到字节码中多了一个默认修饰权限并且名为 access$100 的静态方法,其接受一个 InnerClassTest 类型的参数,即其接受一个外部类对象作为参数,方法内部用三条指令取到参数对象的 field2 字段的值并返回。由此,我们现在大概能猜到内部类对象是怎么取到外部类的 private 权限的字段了:就是通过这个外部类提供的静态方法。类似的,我们注意到 24 行字节码指令 invokestatic ,这里代表执行了一个静态方法,而后面的注释也写的很清楚,调用的是 InnerClassTest$InnerClassA.access$000 方法,即调用了内部类中名为 access$000 的静态方法,根据我们上面的外部类字节码规律,我们也能猜到这个方法就是内部类编译过程中编译器自动生成的,那么我们赶紧来看一下 InnerClassTest$InnerClassA 类的字节码吧:果然,我们在这里发现了名为 access$000 的静态方法,并且这个静态方法接受一个 InnerClassTest$InnerClassA 类型的参数,方法的作用也很简单:返回参数代表的内部类对象的 x2 字段值。我们还注意到编译器给内部类提供了一个接受 InnerClassTest 类型对象(即外部类对象)的构造方法,内部类本身还定义了一个名为 this$0 的 InnerClassTest 类型的引用,这个引用在构造方法中指向了参数所对应的外部类对象。最后,我们在 25 行字节码指令发现:内部类的构造方法通过 invokestatic 指令执行外部类的 access$100 静态方法(在 InnerClassTest 的字节码中已经介绍了)得到外部类对象的 field2 字段的值,并且在后面赋值给 x2 字段。这样的话内部类就成功的通过外部类提供的静态方法得到了对应外部类对象的 field2 。上面我们只是对普通内部类进行了分析,但其实匿名内部类和局部内部类的原理和普通内部类是类似的,只是在访问上有些不同:外部类无法访问匿名内部类和局部内部类对象的字段,因为外部类根本就不知道匿名内部类 / 局部内部类的类型信息(匿名内部类的类名被隐匿,局部内部类只能在定义域内使用)。但是匿名内部类和局部内部类却可以访问外部类的私有成员,原理也是通过外部类提供的静态方法来得到对应外部类对象的私有成员的值。而对于静态内部类来说,因为其实独立于外部类对象而存在,因此编译器不会为静态内部类对象提供外部类对象的引用,因为静态内部类对象的创建根本不需要外部类对象支持。但是外部类对象还是可以访问静态内部类对象的私有成员,因为外部类可以知道静态内部类的类型信息,即可以得到静态内部类的对象,那么就可以通过静态内部类提供的静态方法来获得对应的私有成员值。来看一个简单的代码证明:public class InnerClassTest {int field1 = 1;private int field2 = 2;public InnerClassTest() {InnerClassA inner = new InnerClassA();int v = inner.x2;}// 这里改成了静态内部类,因而不能访问外部类的非静态成员public static class InnerClassA {private int x2 = 0;}}同样的编译步骤,得到了两个 .class 文件,这里看一下内部类的 .class 文件反编译的字节码 InnerClassTest$InnerClassA:仔细看一下,确实没有找到指向外部类对象的引用,编译器只为这个静态内部类提供了一个无参构造方法。而且因为外部类对象需要访问当前类的私有成员,编译器给这个静态内部类生成了一个名为 access$000 的静态方法,作用已不用我多说了。如果我们不看类名,这个类完全可以作为一个普通的外部类来看,这正是静态内部类和其余的内部类的区别所在:静态内部类对象不依赖其外部类对象存在,而其余的内部类对象必须依赖其外部类对象而存在。OK,到这里问题都得到了解释:在非静态内部类访问外部类私有成员 / 外部类访问内部类私有成员 的时候,对应的外部类 / 外部类会生成一个静态方法,用来返回对应私有成员的值,而对应外部类对象 / 内部类对象通过调用其内部类 / 外部类提供的静态方法来获取对应的私有成员的值。内部类和多重继承我们已经知道,Java 中的类不允许多重继承,也就是说 Java 中的类只能有一个直接父类,而 Java 本身提供了内部类的机制,这是否可以在一定程度上弥补 Java 不允许多重继承的缺陷呢?我们这样来思考这个问题:假设我们有三个基类分别为 A、B、C,我们希望有一个类 D 达成这样的功能:通过这个 D 类的对象,可以同时产生 A 、B 、C 类的对象,通过刚刚的内部类的介绍,我们也应该想到了怎么完成这个需求了,创建一个类 D.java:class A {}class B {}class C {}public class D extends A {// 内部类,继承 B 类class InnerClassB extends B {}// 内部类,继承 C 类class InnerClassC extends C {}// 生成一个 B 类对象public B makeB() {return new InnerClassB();}// 生成一个 C 类对象public C makeC() {return new InnerClassC();}public static void testA(A a) {// ...}public static void testB(B b) {// ...}public static void testC(C c) {// ...}public static void main(String[] args) {D d = new D();testA(d);testB(d.makeB());testC(d.makeC());}}程序正确运行。而且因为普通内部类可以访问外部类的所有成员并且外部类也可以访问普通内部类的所有成员,因此这种方式在某种程度上可以说是 Java 多重继承的一种实现机制。但是这种方法也是有一定代价的,首先这种结构在一定程度上破坏了类结构,一般来说,建议一个 .java 文件只包含一个类,除非两个类之间有非常明确的依赖关系(比如说某种汽车和其专用型号的轮子),或者说一个类本来就是为了辅助另一个类而存在的(比如说上篇文章介绍的 HashMap 类和其内部用于遍历其元素的 HashIterator 类),那么这个时候使用内部类会有较好代码结构和实现效果。而在其他情况,将类分开写会有较好的代码可读性和代码维护性。内部类和内存泄露在这一小节开始前介绍一下什么是内存泄露:即指在内存中存在一些其内存空间可以被回收的对象因为某些原因又没有被回收,因此产生了内存泄露,如果应用程序频繁发生内存泄露可能会产生很严重的后果(内存中可用的空间不足导致程序崩溃,甚至导致整个系统卡死)。听起来怪吓人的,这个问题在一些需要开发者手动申请和释放内存的编程语言(C/C++)中会比较容易产生,因为开发者申请的内存需要手动释放,如果忘记了就会导致内存泄露,举个简单的例子(C++):#include int main() {// 申请一段内存,空间为 100 个 int 元素所占的字节数int *p = new int[100];// C++ 11p = nullptr;return 0;}在这段代码里我有意而为之:在为指针 p 申请完内存之后将其直接赋值为 nullptr ,这是 C++ 11 中一个表示空指针的关键字,我们平时常用的 NULL 只是一个值为 0 的常量值,在进行方法重载传参的时候可能会引起混淆。之后我直接返回了,虽然在程序结束之后操作系统会回收我们程序中申请的内存,但是不可否认的是上面的代码确实产生了内存泄露(申请的 100 个 int 元素所占的内存无法被回收)。这只是一个最简单不过的例子。我们在写这类程序的时候当动态申请的内存不再使用时,应该要主动释放申请的内存:#include int main() {// 申请一段内存,空间为 100 个 int 元素所占的字节数int *p = new int[100];// 释放 p 指针所指向的内存空间delete[] p;// C++ 11p = nullptr;return 0;}而在 Java 中,因为 JVM 有垃圾回收功能,对于我们自己创建的对象无需手动回收这些对象的内存空间,这种机制确实在一定程度上减轻了开发者的负担,但是也增加了开发者对 JVM 垃圾回收机制的依赖性,从某个方面来说,也是弱化了开发者防止内存泄露的意识。当然,JVM 的垃圾回收机制的利是远远大于弊的,只是我们在开发过程中不应该丧失了这种对象和内存的意识。回到正题,内部类和内存泄露又有什么关系呢?在继续阅读之前,请确保你对 JVM 的在进行垃圾回收时如何找出内存中不再需要的对象有一定的了解,如果你对这个过程不太了解,你可以参考一下 这篇文章 中对这个过程的简单介绍。我们在上面已经知道了,创建非静态内部类的对象时,新建的非静态内部类对象会持有对外部类对象的引用,这个我们在上面的源码反编译中已经介绍过了,正是因为非静态内部类对象会持有外部类对象的引用,因此如果说这个非静态内部类对象因为某些原因无法被回收,就会导致这个外部类对象也无法被回收,这个听起来是有道理的,因为我们在上文也已经介绍了:非静态内部类对象依赖于外部类对象而存在,所以内部类对象没被回收,其外部类对象自然也不能被回收。但是可能存在这种情况:非静态内部类对象在某个时刻已经不在被使用,或者说这个内部类对象可以在不影响程序正确运行的情况下被回收,而因为我们对这个内部类的使用不当而使得其无法被 JVM 回收,同时会导致其外部类对象无法被回收,即为发生内存泄露。那么这个 “使用不当” 具体指的是哪个方面呢?看一个简单的例子,新建一个 MemoryLeakTest 的类:public class MemoryLeakTest {// 抽象类,模拟一些组件的基类abstract static class Component {final void create() {onCreate();}final void destroy() {onDestroy();}// 子类实现,模拟组件创建的过程abstract void onCreate();// 子类实现,模拟组件摧毁的过程abstract void onDestroy();}// 具体某个组件static class MyComponent extends Component {// 组件中窗口的单击事件监听器static OnClickListener clickListener;// 模拟组件中的窗口MyWindow myWindow;@Overridevoid onCreate() {// 执行组件内一些资源初始化的代码clickListener = new OnClickListener() {@Overridepublic void onClick(Object obj) {System.out.println("对象 " + obj + " 被单击");}};// 新建我的窗口对象,并设置其单击事件监听器myWindow = new MyWindow();myWindow.setClickListener(clickListener);}@Overridevoid onDestroy() {// 执行组件内一些资源回收的代码myWindow.removeClickListener();}}// 我的窗口类,模拟一个可视化控件static class MyWindow {OnClickListener clickListener;// 设置当前控件的单击事件监听器void setClickListener(OnClickListener clickListener) {this.clickListener = clickListener;}// 移除当前控件的单击事件监听器void removeClickListener() {this.clickListener = null;}}// 对象的单击事件的监听接口public interface OnClickListener {void onClick(Object obj);}public static void main(String[] args) {MyComponent myComponent = new MyComponent();myComponent.create();myComponent.destroy();// myComponent 引用置为 null,排除它的干扰myComponent = null;// 调用 JVM 的垃圾回收动作,回收无用对象System.gc();System.out.println("");}}我们在代码中添加一些断点,然后采用 debug 模式查看:程序执行到 72 行代码,此时 72 行代码还未执行,因此 myComponent 引用和其对象还未创建,继续执行:这里成功创建了一个 MyComponent 对象,但是其 create 方法还未执行,所以 myWindow 字段为 null,这里可能有小伙伴会问了,myComponent 对象的 clickListener 字段呢?怎么不见了?其实这和我们在代码中定义 clickListener 字段的形式有关,我们定义的是 static OnClickListener clickListener; ,因此 clickListener 是一个静态字段,其在类加载的完成的时候储存在 JVM 中内存区域的 方法区 中,而创建的 Java 对象储存在 JVM 的堆内存中,两者不在同一块内存区域。关于这些细节,想深入了解的小伙伴建议阅读《深入理解JVM虚拟机》。好了,我们继续执行代码:myComponent.create 方法执行完成之后创建了 OnClickListener 内部类对象,并且为 myWindow 对象设置 OnCLickListener 单击事件监听。我们继续:myComponent.destroy 方法执行完成之后,myWindow.removeClickListener 方法也执行完成,此时 myWindow 对象中的 clickListener字段为 null。我们继续:代码执行到了 80 行,在此之前,所有的代码和解释都没有什么难度,跟着运行图走,一切都那么顺利成章,其实这张图的运行结果也很好理解,只不过图中的文字需要思考一下:myComponent 引用指向的对象真的被回收了吗?要解答这个问题,我们需要借助 Java 中提供的内存分析工具 jvisualvm (以前它还不叫这个名字…),它一般在你安装 JDK 的目录下的 bin 子目录下:我们运行这个程序:在程序左边可以找到我们当前正在执行的 Java 进程,双击进入:单击 tab 中的 监视 选项卡,可以看到当前正在执行的 Java 进程的一些资源占用信息,当然我们现在的主要目的是分析内存,那么们单击右上角的 堆 Dump :在这个界面,单击 类 选项卡,会出现当前 Java 进程中用到的所有的类,我们已经知道我们要查找的类的对象只创建了一个,因此我们根据右上角的 实例数 来进行排除:我们成功的找到了我们创建的对象!而这样也意味着当我们在上面代码中调用 JVM 的垃圾回收动作没有回收这三个对象,这其实就是一个真真切切的内存泄露!因为我们将 main 方法中的 myComponent 引用赋值为 null,就意味着我们已经不再使用这个组件和里面的一些子组件(MyWindow 对象),即这个组件和其内部的一些组件应该被回收。但是调用 JVM 的垃圾回收却并没有将其对应的对象回收。造成这个问题的原因在哪呢?其实就在于我们刚刚在 MyComponent 类中定义的 clickListener 字段,我们在代码中将其定义成了 static 类型的,同时这个字段又指向了一个匿名内部类对象(在 create 方法中 创建了一个 OnClickListener 接口对象,即通过一个匿名内部类实现这个接口并创建其对象),根据 JVM 寻找和标记无用对象的规则(可达性分析算法),其会将 clickListener 字段作为一个 “root” ,并通过它来寻找还有用的对象,在这个例子中,clickListener 字段指向一个匿名内部类对象,这个匿名内部类对象有一个外部类对象(MyComponent 类型的对象)的引用,而外部类对象中又有一个 MyWindow 类型的对象引用。因此 JVM 会将这三个对象都视为有用的对象不会回收。用图来解释吧:Ok,通过这个过程,相信你已经理解了造成此次内存泄露的原因了,那么我们该如何解决呢?对于当前这个例子,我们只需要改一些代码:把 MyComponent 类中的 clickListener 字段前面的 static 修饰符去掉就可以了(static OnClickListener clickListener; -> OnClickListener clickListener;),这样的话 clickListener 指向的对象,就作为 MyComponent 类的对象的一部分了,在 MyComponent 对象被回收时里面的子组件也会被回收。同时它们之间也只是互相引用(MyComponent 外部类对象中有一个指向 OnClickListener 内部类对象的引用,OnClickListener 内部类对象有一个指向 MyComponent 外部类对象的引用),根据 JVM 的 “可达性分析” 算法,在两个对象都不再被外部使用时,JVM 的垃圾回收机制是可以标记并回收这两个对象的。 虽然不强制要求你在 MyComponent 类中的 onDestroy 方法中将其 clickListener 引用赋值为 null,但是我还是建议你这样做,因为这样更能确保你的程序的安全性(减少发生内存泄露的机率,毕竟匿名内部类对象会持有外部类对象的引用),在某个组件被销毁时将其内部的一些子组件进行合理的处理是一个很好的习惯。你也可以自定义一个静态内部类或者是另外自定义一个类文件,并实现 OnClickListener 接口,之后通过这个类创建对象,这样就可以避免通过非静态内部类的形式创建 OnClickListener 对象增加内存泄露的可能性。避免内存泄漏那么我们在日常开发中怎么合理的使用内部类来避免产生内存泄露呢?这里给出一点我个人的理解:能用静态内部类就尽量使用静态内部类,从上文中我们也知道了,静态内部类的对象创建不依赖外部类对象,即静态内部对象不会持有外部类对象的引用,自然不会因为静态内部类对象而导致内存泄露,所以如果你的内部类中不需要访问外部类中的一些非 static 成员,那么请把这个内部类改造成静态内部类;对于一些自定义类的对象,慎用 static 关键字修饰(除非这个类的对象的声明周期确实应该很长),我们已经知道,JVM 在进行垃圾回收时会将 static 关键字修饰的一些静态字段作为 “root” 来进行存活对象的查找,所以程序中 static 修饰的对象越多,对应的 “root” 也就越多,每一次 JVM 能回收的对象就越少。 当然这并不是建议你不使用 static 关键字,只是在使用这个关键字之前可以考虑一下这个对象使用 static 关键字修饰对程序的执行确实更有利吗?为某些组件(大型)提供一个当这个大型组件需要被回收的时候用于合理处理其中的一些小组件的方法(例如上面代码中 MyComponent 的 onDestroy 方法),在这个方法中,确保正确的处理一些需要处理的对象(将某些引用置为 null、释放一些其他(CPU…)资源)————————————————原文链接:https://blog.csdn.net/weixin_35998791/article/details/114450261
  • [技术干货] JAVA SE 多线程
    首先,我们设想以下的一个场景:当一家公司去银行办理业务,既要进行财务转账,⼜要进行福利发放,还得进行缴社保。如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五⼀起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理同一家公司的业务。此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三⼀般被称为主线程(Main Thread)。为什么要有线程呢?首先, “并发编程” 成为 “刚需” ,单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源 . 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要用到并发编程.其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量创建线程比创建进程更快.销毁线程比销毁进程更快.调度线程比调度进程更快最后, 线程虽然比进程轻量, 但是人们还不满足 , 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)进程和线程的区别?进程是包含线程的. 每个进程至少有一个线程存在,即主线程。进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.进程是系统分配资源的最小单位,线程是系统调度的最小单位。一个进程挂了一般不会影响到其他进程. 但是一个线程挂了, 可能把同进程内的其他线程一起带走(整个进程崩溃).Java 的线程 和 操作系统线程 的关系?线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用 , Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.1. Thread类及常见方法Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread 对象与之关联。用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。1.1 创建线程继承Thread类//继承Thread来创建一个线程类class MyThread extends Thread{    @Override    //重新run方法,run方法中是该线程具体要做的任务    public void run() {        for (int i = 0; i < 100; i++) {            System.out.println(i);        }    }}public class Test {    public static void main(String[] args) {        //实例化线程类对象        MyThread t = new MyThread();        //通过start()方法启动线程        t.start();    }}实现 Runnable 接口class MyRunnable implements Runnable{    @Override    public void run() {        for (int i = 0; i < 100; i++) {            System.out.println(i);        }    }}public class Test {    public static void main(String[] args){        //创建 Thread 类实例, 调⽤ Thread 的构造⽅法时将 Runnable 对象作为 target 参数        Thread t = new Thread(new MyRunnable());        t.start();    }}匿名内部类创建 Thread 子类对象public class Test {    public static void main(String[] args) {        //匿名内部类创建Thread的子类        Thread t = new Thread(){            @Override            public void run() {                for (int i = 0; i < 100; i++) {                    System.out.println(i);                }            }        };        t.start();    }}匿名内部类创建 Runnable 子类对象public class Test {    public static void main(String[] args) {        Thread t = new Thread(new Runnable() {            @Override            public void run() {                for (int i = 0; i < 100; i++) {                    System.out.println(i);                }            }        });        t.start();    }}lambda 表达式创建 Runnable 子类对象public class Test {    public static void main(String[] args) {        Thread t = new Thread(()->{            System.out.println("这是一个用lambda表达式创建的线程");        });        t.start();    }}强烈推荐!!!1.2 Thread 的常见构造方法方法 说明Thread() 创建线程对象Thread(String name) 创建线程对象并命名Thread(Runnable target , String name) 使用Runnable对象创建线程对象并命名Thread(Runnable target) 使用Runnable对象创建线程对象1.3 Thread 的几个常见属性• ID 是线程的唯一标识,不同线程不会重复• 名称是什么无所谓,不影响运行,是为了方便调试• 状态表示线程当前所处的一个情况• 优先级高的线程理论上来说更容易被调度到• 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。• 是否存活,即简单的理解,为 run 方法是否运行结束了1.4 启动一个线程—start()我们现在已经知道如何通过覆写 run 方法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。• 覆写 run 方法是提供给线程要做的事情的指令清单• 线程对象可以认为是把 李四、王五叫过来了• 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。调用 start 方法, 才真的在操作系统的底层创建出一个线程.1.5 中断一个线程—interrupt()通过一个变量进行标记public class Test {    public static boolean flag = true;    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(){            @Override            public void run() {                while(flag){                    System.out.println("hello thread");                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        };        t.start();        System.out.println("hello main");        Thread.sleep(3000);        flag = false;        System.out.println("让线程中断");    }}调用 interrupt() 方法public class Test {    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(()->{            // 由于这个 currentThread 方法, 是在后续 t start 之后, 才执行的.            // 并且是在 t 线程中执行的. 返回的结果就是指向 t 线程对象的引用了.            while(!Thread.currentThread().isInterrupted()){                System.out.println("hello thread");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                    break;                }            }        });        t.start();        Thread.sleep(2000);        //调用这个方法,就是把标志位由false改为true        t.interrupt();    }}//使用interrupt()方法的时候//1. t线程没有进行sleep()阻塞时,t的isInterrupted()方法返回true,通过循环条件结束循环//2. t线程进行sleep()阻塞时,t的isInterrupted()方法还是返回true,但是sleep()方法如果被提前唤醒,抛出InterruptedException异常,同时会把isInterrupted()方法设为false,此时就要手动决定是否要结束线程了1.6 等待一个线程—join()有时,我们需要等待一个线程完成它的工作后,才能进行自己的下⼀步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。public class Test {    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(()->{            for (int i = 0; i < 4; i++) {                System.out.println("hello thread");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        t.start();        System.out.println("main线程开始了");        t.join();        System.out.println("main线程等t线程结束了");    }}1.7 获取当前线程引用1.8 休眠当前线程方法 解释public static native void sleep(long millis) throws InterruptedException; 休眠当前线程 , 以毫米为单位2. 线程的状态线程的状态是一个枚举类型:• NEW: 安排了工作, 还未开始行动• RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作• BLOCKED: 由于加锁产生的阻塞• WAITING: 无超时时间的阻塞• TIMED_WAITING:有超时时间的阻塞• TERMINATED: 工作完成了3. 线程安全3.1 线程不安全案例请大家观察下述代码:public class Test {    private static int count;    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(()->{            for (int i = 0; i < 50_000; i++) {                count++;            }        });        Thread t2 = new Thread(()->{            for (int i = 0; i < 50_000; i++) {                count++;            }        });        t1.start();        t2.start();        t1.join();        t2.join();        System.out.println(count);    }}大家认为count最终的值会是100_000吗? 不是的,count最终的值是一个小于100_000的随机数.那为什么呢?线程不安全的概念?如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。3.2 线程不安全的原因线程调度是随机的修改共享数据即多个线程同时修改一个数据原子性什么是原子性?我们把一段代码想象成一个房间,每个线程就是要进入这个房间的⼈。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。有时也把这个现象叫做同步互斥,表示操作是互相排斥的。一条 java 语句不一定是原子的,也不一定只是一条指令上述代码中的count++对应着3条指令:load : 从内存把数据读到 CPUadd : 进行数据更新save : 把数据写回到 CPU上述三条指令在多线程中就是有问题的指令.如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。将3种指令执行顺序枚举出我们发现:只有第一种和第二种是正确的内存可见性这里主要个大家介绍一下JMM模型,关于可见性内容请大家查阅目录找volatile关键字Java 内存模型 (JMM—Java Memory Model):Java虚拟机规范中定义了Java内存模型 , 目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.• 线程之间的共享变量存在 主内存 (Main Memory)• 每一个线程都有自己的 “工作内存” (Working Memory)• 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.• 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.因为每个线程有自己的工作内存, 这些工作内存中的内容相当于同⼀个共享变量的 “副本”. 这就导致了此时修改线程1 的工作内存中的值, 线程2 的工作内存不⼀定会及时变化.初始情况下, 两个线程的工作内存内容一致.一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不⼀定能及时同步此时就引入了三个问题:1.为什么要整这么多内存呢?实际并没有这么多 “内存”. 这只是 Java 规范中的一个术语, 是属于 “抽象” 的叫法.所谓的 “主内存” 才是真正硬件角度的 “内存”. 而所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存.CPU的寄存器和缓存统称为工作内存,越往上,速度越快,空间越小,成本越高2.为啥要这么麻烦的拷来拷去?因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级,也就是几千倍, 上万倍).比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了.效率就大大提高了.3.那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥??答案就是⼀个字: 贵 , 快和慢都是相对的. CPU 访问寄存器速度远远快于内存, 但是内存的访问速度⼜远远快于硬盘.对应的, CPU 的价格最贵, 内存次之, 硬盘最便宜.指令重排序一段代码是这样的:去前台取下 U 盘去教室写 10 分钟作业去前台取下快递如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题的,可以少跑⼀次前台。这种叫做指令重排序.关于指令重排序引发的线程不安全问题请查询目录到单例模式!!!4. synchronized 关键字4.1 synchronized的特性互斥synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同⼀个对象 synchronized 就会阻塞等待.synchronized用的锁是存在Java对象头里的。可以粗略理解成, 每个对象在内存中存储的时候, 都存有⼀块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”).如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.如果当前是 “有人” 状态, 那么其他⼈无法使用, 只能排队什么是阻塞等待呢?针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程,再来获取到这个锁.假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁,而是和 C 重新竞争, 并不遵守先来后到的规则.可重入synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题;什么是自己把自己锁死?//第一次加锁,命名为锁1synchronized (locker){//第二次尝试加锁,命名为锁2,但是此时加锁要等到锁1释放锁synchronized (locker){count++;}}//锁1释放锁的条件锁2中的代码要执行完,这就是自己把自己锁死了//理解一下这个场景,车钥匙在家里,家门钥匙在车里但Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.举个例子:加入我追x姑娘,此时x姑娘处于未加锁状态 , 我可以表白成功 , 其他人也可以表白成功 . 但是如果我表白成功了, 意味着x姑娘就处于加锁状态了 , 其他人在想表白是不可能成功的 , 但是我无论想在表白多少次 , x姑娘都会同意在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息:• 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.• 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)4.2 synchronized 使用示例修饰代码块public class SynchronizedDemo {private Object locker = new Object();   public void method() {  synchronized (locker) {   }  }}直接修饰普通方法public class SynchronizedDemo {  public synchronized void methond() {}}修饰静态方法public class SynchronizedDemo {  public synchronized static void method() {}}5. volatile关键字内存可见性import java.util.Scanner; class Counter {    public int flag = 0;}public class Test {    public static void main(String[] args) throws InterruptedException {        Counter count = new Counter();        Thread t1 = new Thread(()->{           while (count.flag == 0){               System.out.println("it is t1 main thread");               try {                   Thread.sleep(1000);               } catch (InterruptedException e) {                   e.printStackTrace();               }           }        });        Scanner scanner = new Scanner(System.in);        Thread t2 = new Thread(()->{            System.out.println("please input a number");            count.flag = scanner.nextInt();        });        t1.start();        t2.start();        t1.join();        t2.join();    }}在这个代码中:• 创建两个线程 t1 和 t2• t1 中包含⼀个循环, 这个循环以 flag == 0 为循环条件.• t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.• 预期当用户输入非 0 的值的时候, t1 线程结束结果发现输入任意一个数字后线程t1并没有停止(这就是一个bug)这是因编译器自身优化导致的bug,当编译器发现我们频繁load的flag是一个值得时候,就会把flag方法工作内存上,就不再上主内存load了,但是我们突然修改flag的值,主内存修改了,但是t1线程的工作内存并没有修改代码在写入 volatile 修饰的变量的时候:• 改变线程工作内存中volatile变量副本的值• 将改变后的副本的值从工作内存刷新到主内存代码在读取 volatile 修饰的变量的时候:• 从主内存中读取volatile变量的最新值到线程的工作内存中• 从工作内存中读取volatile变量的副本前⾯我们讨论JMM模型时说了, 线程直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况. 加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.volatile不能保证原子性虽然volatile解决了内存可见性,但是volatile不是原子的,我们想解决原子性问题还要synchronized锁,volatile和synchronized是两个不同维度的问题6. wait与notify因为线程都是抢占式进行的,并没有固定的顺序,是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.例如一场篮球赛 : 我们要让A球员传球 , B球员拿到球后进行投篮完成这个协调工作, 主要涉及到以下的方法:• wait() / wait(long timeout): 让当前线程进入等待状态.• notify() / notifyAll(): 唤醒在当前对象上等待的线程.注意: wait, notify, notifyAll 都是 Object 类的方法6.1 wait()方法wait 做的事情:• 使当前执行代码的线程进行等待. (把线程放到等待队列中)• 释放当前的锁• 满足一定条件时被唤醒, 重新尝试获取这个锁.注意 : wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.wait 结束等待的条件:• 其他线程调用该对象的 notify 方法.• wait 等待时间超时 (wait 方法提供⼀个带有 timeout 参数的版本, 来指定等待时间).• 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.6.2 notify()方法notify 方法是唤醒等待的线程.• 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。• 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)• 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。6.3 notifyAll()方法notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程notifyAll 一下全都唤醒, 需要这些线程重新竞争锁7. 单例模式首先我们要知道 , 什么是设计模式?设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.单例模式具体的实现方式有很多. 最常见的是 “饿汉” 和 “懒汉” 两种.饿汉模式//类加载的同时创建实例class Singleton{    private Singleton instance = new Singleton();    private Singleton(){};    public Singleton getInstance(){        return instance;    }}懒汉模式—单线程版//类加载的时候不创建实例. 第一次使⽤的时候才创建实例class Singleton{    private static Singleton instence = null;    private Singleton(){};    public static Singleton getInstance(){        if (instence == null){            return new Singleton();        }        return instence;    }}1234567891011懒汉模式—多线程版//使⽤双重 if 判定, 降低锁竞争的频率.//给 instance 加上了 volatile.class Singleton{    private static Object locker = new Object();    private static volatile Singleton instence = null;    private Singleton(){};    public static Singleton getInstance(){                if (instence==null){            synchronized (locker){                if (instence == null){                    return new Singleton();                }            }        }        return instence;    }}理解双重 if 判定:加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了. 外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了.当多线程首次调⽤ getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作.当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.8. 阻塞队列什么是阻塞队列?阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:• 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.• 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.阻塞队列的⼀个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.8.1 生产者消费者模型生产者消费者模式就是通一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力. (削峰填谷)阻塞队列也能使生产者和消费者之间 解耦.8.2 标准库中的阻塞队列在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.生产者消费者模型:import java.util.Random;import java.util.concurrent.BlockingQueue;import java.util.concurrent.LinkedBlockingQueue;public class Test {    public static void main(String[] args) {        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();        Thread producer = new Thread(()->{            Random random = new Random();           while (true){               try {                   int value = random.nextInt(1000);                   blockingQueue.put(value);                   System.out.println("生产了:"+value);                   Thread.sleep(5000);               } catch (InterruptedException e) {                   e.printStackTrace();               }           }        });        Thread consumer = new Thread(()->{           while (true){               try {                   int value = blockingQueue.take();                   System.out.println("消费了:"+value);                   Thread.sleep(6000);               } catch (InterruptedException e) {                   e.printStackTrace();               }           }        });        producer.start();        consumer.start();    }}8.3 阻塞队列实现• 通过 “循环队列” 的方式来实现.• 使用 synchronized 进行加锁控制.• put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).• take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)public class BlockingQueue {  private int[] items = new int[1000];  private volatile int size = 0;  private volatile int head = 0;private volatile int tail = 0;   public void put(int value) throws InterruptedException {  synchronized (this) {  // 此处最好使⽤ while.  // 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,  // 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能⼜已经队列满了就只能继续等待  while (size == items.length) {  wait();  }  items[tail] = value;  tail = (tail + 1) % items.length;  size++;  notifyAll();}}public int take() throws InterruptedException {int ret = 0;synchronized (this) {while (size == 0) {wait();}ret = items[head];head = (head + 1) % items.length;size--;notifyAll();}return ret;}public synchronized int size() {return size;}}9. 定时器定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.比如⼀个 Map, 希望⾥⾯的某个 key 在 3s 之后过期(自动删除).类似于这样的场景就需要用到定时器.标准库中的定时器:标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule , schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).Timer timer = new Timer();timer.schedule(new TimerTask() {  @Overridepublic void run() {  System.out.println("hello");  }}, 3000);10.线程池虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效.线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到⼀个 “池子” 中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了.10.1 ExecutorService 和 ExecutorsExecutorService 表示一个线程池实例.Executors 是一个工厂类, 能够创建出几种不同风格的线程池.ExecutorService 的 submit 方法能够向线程池中提交若干个任务. ExecutorService service = Executors.newFixedThreadPool(1);        service.submit(()->{            System.out.println("this is a service");        });Executors 创建线程池的几种方式:newFixedThreadPool: 创建固定线程数的线程池newCachedThreadPool: 创建线程数目动态增长的线程池.newSingleThreadExecutor: 创建只包含单个线程的线程池newScheduledThreadPool: 设定延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.Executors 本质上是 ThreadPoolExecutor 类的封装10.2 ThreadPoolExecutorThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定.把创建一个线程池想象成开个公司. 每个员工相当于一个线程.corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退).keepAliveTime: 临时工允许的空闲时间.unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.workQueue: 传递任务的阻塞队列threadFactory: 创建线程的工厂, 参与具体的创建线程⼯作RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.◦ AbortPolicy(): 超过负荷, 直接抛出异常.◦ CallerRunsPolicy(): 调用者负责处理◦ DiscardOldestPolicy(): 丢弃队列中最老的任务.◦ DiscardPolicy(): 丢弃新来的任务.————————————————原文链接:https://blog.csdn.net/2401_82690001/article/details/147980210
  • [技术干货] Java+Selenium+快代理实现高效爬虫
    一、前言在Web爬虫技术中,Selenium作为一款强大的浏览器自动化工具,能够模拟真实用户操作,有效应对JavaScript渲染、Ajax加载等复杂场景。而集成代理服务则能够解决IP限制、地域访问限制等问题。本文将详细介绍如何利用Java+Selenium+快代理实现高效的爬虫系统。二、Selenium简介Selenium是一个用于Web应用程序自动化测试的工具集,它主要用于自动化浏览器操作,可以模拟用户与网页的交互行为,如点击按钮、填写表单、滚动页面等。在爬虫领域,Selenium特别适合处理那些需要JavaScript渲染、需要登录或有反爬措施的网站。三、环境准备JDK1.8Maven项目管理相关依赖<!-- Selenium --><dependency>    <groupId>org.seleniumhq.selenium</groupId>    <artifactId>selenium-java</artifactId>    <version>3.141.59</version></dependency><dependency>    <groupId>io.github.bonigarcia</groupId>    <artifactId>webdrivermanager</artifactId>    <version>5.3.2</version></dependency>四、代码实现本系统采用的是工厂模式创建WebDriver实例,这样做的好处主要是可以提供统一的创建方法,不管使用那种浏览器都适用,自由配置。其次就是维护方便,浏览器配置变更只需修改工厂类中的相关方法,扩展性也不错,可以轻松添加新的浏览器支持,比如Opera或者Safari等等。4.1 创建WebDriver工厂类import io.github.bonigarcia.wdm.WebDriverManager;import org.openqa.selenium.Proxy;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.chrome.ChromeOptions;import org.openqa.selenium.edge.EdgeDriver;import org.openqa.selenium.edge.EdgeOptions;import org.openqa.selenium.firefox.FirefoxDriver;import org.openqa.selenium.firefox.FirefoxOptions;import org.openqa.selenium.remote.CapabilityType;import org.openqa.selenium.remote.PageLoadStrategy;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.concurrent.TimeUnit;/** * WebDriver工厂类,负责创建和配置各种浏览器驱动实例 * 设计思路: * 1. 使用工厂模式统一管理不同浏览器的WebDriver创建逻辑 * 2. 采用构建器模式(Builder Pattern)使配置更加灵活 * 3. 封装复杂的浏览器选项设置,简化调用代码 * 4. 支持多种浏览器类型和代理配置 *  * 好处: * 1. 代码复用性高,减少重复代码 * 2. 配置灵活,通过链式调用设置参数 * 3. 职责单一,仅负责创建WebDriver * 4. 易于扩展,可轻松添加新的浏览器类型支持 */public class WebDriverFactory {    // 使用SLF4J记录日志,便于问题排查    private static final Logger log = LoggerFactory.getLogger(WebDriverFactory.class);        // 默认配置,可通过构建器方法修改    private boolean headless = true;                // 默认无头模式    private int pageLoadTimeoutSeconds = 30;        // 页面加载超时时间    private int scriptTimeoutSeconds = 30;          // 脚本执行超时时间    private int implicitWaitSeconds = 10;           // 隐式等待时间        // 代理配置    private boolean proxyEnabled = false;           // 是否启用代理    private String proxyHost;                       // 代理主机地址    private int proxyPort;                          // 代理端口    private String proxyUsername;                   // 代理用户名(认证用)    private String proxyPassword;                   // 代理密码(认证用)        /**     * 支持的浏览器类型枚举     * 便于扩展,后续可以增加其他浏览器支持     */    public enum BrowserType {        CHROME, EDGE, FIREFOX    }        /**     * 设置是否使用无头模式     * 无头模式下浏览器不会显示界面,更加节省资源     *      * @param headless true表示使用无头模式,false表示显示浏览器界面     * @return 当前工厂实例,支持链式调用     */    public WebDriverFactory withHeadless(boolean headless) {        this.headless = headless;        return this;    }        /**     * 设置页面加载超时时间     *      * @param seconds 超时秒数     * @return 当前工厂实例,支持链式调用     */    public WebDriverFactory withPageLoadTimeout(int seconds) {        this.pageLoadTimeoutSeconds = seconds;        return this;    }        /**     * 设置JavaScript脚本执行超时时间     *      * @param seconds 超时秒数     * @return 当前工厂实例,支持链式调用     */    public WebDriverFactory withScriptTimeout(int seconds) {        this.scriptTimeoutSeconds = seconds;        return this;    }        /**     * 设置元素查找隐式等待时间     *      * @param seconds 等待秒数     * @return 当前工厂实例,支持链式调用     */    public WebDriverFactory withImplicitWait(int seconds) {        this.implicitWaitSeconds = seconds;        return this;    }        /**     * 配置代理服务器     *      * @param host 代理主机地址     * @param port 代理端口     * @return 当前工厂实例,支持链式调用     */    public WebDriverFactory withProxy(String host, int port) {        this.proxyEnabled = true;        this.proxyHost = host;        this.proxyPort = port;        return this;    }        /**     * 配置代理服务器认证信息     *      * @param username 代理用户名     * @param password 代理密码     * @return 当前工厂实例,支持链式调用     */    public WebDriverFactory withProxyAuth(String username, String password) {        this.proxyUsername = username;        this.proxyPassword = password;        return this;    }        /**     * 创建指定类型的WebDriver实例     * 工厂方法核心,根据指定的浏览器类型创建对应的WebDriver     *      * @param browserType 浏览器类型枚举     * @return 配置好的WebDriver实例     */    public WebDriver createWebDriver(BrowserType browserType) {        switch (browserType) {            case CHROME:                return createChromeDriver();            case EDGE:                return createEdgeDriver();            case FIREFOX:                return createFirefoxDriver();            default:                // 默认使用Edge浏览器                log.info("未指定浏览器类型,默认使用Edge浏览器");                return createEdgeDriver();        }    }        /**     * 创建Edge浏览器WebDriver实例     *      * @return 配置好的Edge WebDriver     */    private WebDriver createEdgeDriver() {        // 自动下载与系统浏览器匹配的WebDriver,避免版本不匹配问题        WebDriverManager.edgedriver().setup();                EdgeOptions options = new EdgeOptions();                // 配置浏览器选项        Map<String, Object> edgePrefs = new HashMap<>();        // 禁用自动扩展,减少资源占用和干扰        edgePrefs.put("useAutomationExtension", false);                // 获取通用浏览器参数        List<String> args = getCommonBrowserArgs();                Map<String, Object> edgeOptions = new HashMap<>();        edgeOptions.put("args", args);                // 设置User-Agent,模拟真实浏览器,减少被网站识别为爬虫的可能        options.setCapability("ms.edge.userAgent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0");                // 设置页面加载策略为NORMAL,确保页面完整加载        // 可选值:NONE (不等待加载), EAGER (DOM就绪即可), NORMAL (等待完全加载)        options.setPageLoadStrategy(PageLoadStrategy.NORMAL);                // Edge特有配置        options.setCapability("ms:edgeChromium", true);        options.setCapability("ms:edgeOptions", edgeOptions);        // 使用隐私模式,避免历史记录、cookie等信息的干扰        options.setCapability("inPrivate", true);                // 配置代理        configureProxy(options);                // 创建WebDriver实例        WebDriver driver = new EdgeDriver(options);        // 配置超时设置        configureTimeouts(driver);                log.info("Edge WebDriver创建成功");        return driver;    }        /**     * 创建Chrome浏览器WebDriver实例     *      * @return 配置好的Chrome WebDriver     */    private WebDriver createChromeDriver() {        // 自动下载与系统浏览器匹配的WebDriver        WebDriverManager.chromedriver().setup();                ChromeOptions options = new ChromeOptions();                // 根据配置决定是否使用无头模式        if (headless) {            options.addArguments("--headless");        }                // 添加通用浏览器参数        for (String arg : getCommonBrowserArgs()) {            options.addArguments(arg);        }                // 设置页面加载策略        options.setPageLoadStrategy(PageLoadStrategy.NORMAL);                // Chrome浏览器特殊处理代理配置        configureProxyForChrome(options);                // 创建WebDriver实例        WebDriver driver = new ChromeDriver(options);        // 配置超时设置        configureTimeouts(driver);                log.info("Chrome WebDriver创建成功");        return driver;    }        /**     * 创建Firefox浏览器WebDriver实例     *      * @return 配置好的Firefox WebDriver     */    private WebDriver createFirefoxDriver() {        // 自动下载与系统浏览器匹配的WebDriver        WebDriverManager.firefoxdriver().setup();                FirefoxOptions options = new FirefoxOptions();                // 根据配置决定是否使用无头模式        if (headless) {            options.addArguments("--headless");        }                // 配置代理        configureProxy(options);                // 创建WebDriver实例        WebDriver driver = new FirefoxDriver(options);        // 配置超时设置        configureTimeouts(driver);                log.info("Firefox WebDriver创建成功");        return driver;    }        /**     * 获取通用浏览器启动参数     * 这些参数适用于基于Chromium的浏览器(Chrome, Edge)     *      * @return 参数列表     */    private List<String> getCommonBrowserArgs() {        List<String> args = new ArrayList<>();                // 无头模式相关参数        if (headless) {            args.add("--headless");  // 不显示浏览器界面            args.add("--disable-gpu");  // 在某些系统上无头模式需要禁用GPU加速        }                // 禁用扩展和插件,减少资源占用和干扰        args.add("--disable-extensions");                // 禁用图片加载,提高性能        args.add("--blink-settings=imagesEnabled=false");                // 解决在Docker容器中可能出现的共享内存问题        args.add("--disable-dev-shm-usage");                // 禁用平滑滚动,减少自动滚动问题        args.add("--disable-smooth-scrolling");                // 设置固定窗口大小,避免响应式变化导致的元素定位问题        args.add("--window-size=1366,768");                // 禁用站点隔离,减少内存使用        args.add("--disable-features=site-per-process");                // 禁用默认应用,减少启动时间        args.add("--disable-default-apps");                // 减少日志输出,提高性能        args.add("--disable-logging");                // 禁用信息栏,避免干扰        args.add("--disable-infobars");                // 禁用通知,避免干扰        args.add("--disable-notifications");                // 添加性能优化参数        args.add("--disable-web-security");  // 禁用同源策略检查        args.add("--no-sandbox");  // 禁用沙箱模式,提高性能(注意安全风险)        args.add("--disable-setuid-sandbox");  // 禁用setuid沙箱,配合--no-sandbox使用        args.add("--disable-accelerated-2d-canvas");  // 禁用加速2D Canvas,减少GPU使用        args.add("--disable-crash-reporter");  // 禁用崩溃报告        args.add("--disable-in-process-stack-traces");  // 禁用进程内堆栈跟踪        args.add("--disable-breakpad");  // 禁用断点调试        args.add("--aggressive-cache-discard");  // 积极丢弃缓存,减少内存使用        args.add("--disable-ipc-flooding-protection");  // 禁用IPC洪水保护                // 限制JavaScript引擎内存使用,防止内存溢出        args.add("--js-flags=--max-old-space-size=512");                return args;    }        /**     * 为浏览器选项配置代理     * 适用于Edge和Firefox浏览器     *      * @param options 浏览器选项对象     */    private void configureProxy(Object options) {        if (proxyEnabled && proxyHost != null && !proxyHost.isEmpty() && proxyPort > 0) {            try {                // 构建代理URL,处理是否需要认证                String proxyUrl;                if (proxyUsername != null && !proxyUsername.isEmpty() && proxyPassword != null) {                    // 带认证的代理格式:http://username:password@host:port                    proxyUrl = "http://" + proxyUsername + ":" + proxyPassword + "@" + proxyHost + ":" + proxyPort;                } else {                    // 不带认证的代理格式:http://host:port                    proxyUrl = "http://" + proxyHost + ":" + proxyPort;                }                                // 创建代理对象                Proxy proxy = new Proxy();                // 同时设置HTTP和HTTPS代理,确保所有请求都通过代理                proxy.setHttpProxy(proxyUrl);                proxy.setSslProxy(proxyUrl);                                // 根据浏览器类型设置代理能力                if (options instanceof EdgeOptions) {                    ((EdgeOptions) options).setCapability(CapabilityType.PROXY, proxy);                } else if (options instanceof FirefoxOptions) {                    ((FirefoxOptions) options).setCapability(CapabilityType.PROXY, proxy);                }                                log.info("WebDriver配置了代理: {}", proxyHost + ":" + proxyPort);            } catch (Exception e) {                log.error("配置代理时出错: {}", e.getMessage());            }        }    }        /**     * 为Chrome浏览器特别配置代理     * Chrome处理代理的方式与Edge和Firefox略有不同     *      * @param options Chrome浏览器选项对象     */    private void configureProxyForChrome(ChromeOptions options) {        if (proxyEnabled && proxyHost != null && !proxyHost.isEmpty() && proxyPort > 0) {            try {                // 构建代理URL,处理是否需要认证                String proxyUrl;                if (proxyUsername != null && !proxyUsername.isEmpty() && proxyPassword != null) {                    // 带认证的代理                    proxyUrl = "http://" + proxyUsername + ":" + proxyPassword + "@" + proxyHost + ":" + proxyPort;                } else {                    // 不带认证的代理                    proxyUrl = "http://" + proxyHost + ":" + proxyPort;                }                                // 创建代理对象                Proxy proxy = new Proxy();                proxy.setHttpProxy(proxyUrl);                proxy.setSslProxy(proxyUrl);                                // 为Chrome设置代理能力                options.setCapability(CapabilityType.PROXY, proxy);                                log.info("Chrome WebDriver配置了代理: {}", proxyHost + ":" + proxyPort);            } catch (Exception e) {                log.error("配置Chrome代理时出错: {}", e.getMessage());            }        }    }        /**     * 配置WebDriver的各种超时设置     *      * @param driver WebDriver实例     */    private void configureTimeouts(WebDriver driver) {        // 设置页面加载超时时间        driver.manage().timeouts().pageLoadTimeout(pageLoadTimeoutSeconds, TimeUnit.SECONDS);        // 设置脚本执行超时时间        driver.manage().timeouts().setScriptTimeout(scriptTimeoutSeconds, TimeUnit.SECONDS);        // 设置隐式等待时间,查找元素时使用        driver.manage().timeouts().implicitlyWait(implicitWaitSeconds, TimeUnit.SECONDS);                log.debug("WebDriver超时配置完成:页面加载={}秒,脚本执行={}秒,隐式等待={}秒",                pageLoadTimeoutSeconds, scriptTimeoutSeconds, implicitWaitSeconds);    }}4.2 创建爬虫主类import org.openqa.selenium.By;import org.openqa.selenium.WebDriver;import org.openqa.selenium.WebElement;import org.openqa.selenium.support.ui.ExpectedConditions;import org.openqa.selenium.support.ui.WebDriverWait;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.List;/** * Selenium爬虫示例主类 * 演示如何使用WebDriverFactory创建浏览器实例并进行网页爬取 */public class SeleniumCrawler {    private static final Logger log = LoggerFactory.getLogger(SeleniumCrawler.class);        public static void main(String[] args) {        // 推荐使用快代理的隧道代理:https://www.kuaidaili.com/?ref=soi1rkc6rd82        String proxyHost = "";  // 快代理隧道代理主机        int proxyPort = 15818;                // 端口,根据实际情况修改        String proxyUsername = "yourUsername"; // 替换为您的快代理用户名        String proxyPassword = "yourPassword"; // 替换为您的快代理密码                // 创建WebDriver工厂实例,配置爬虫参数        // 使用构建器模式,代码可读性强,配置灵活        WebDriverFactory factory = new WebDriverFactory()            .withHeadless(false)  // 设置为false可以看到浏览器界面,方便调试            .withPageLoadTimeout(30)  // 页面加载超时设置为30秒            .withScriptTimeout(30)    // 脚本执行超时设置为30秒              .withImplicitWait(10)     // 查找元素隐式等待10秒            .withProxy(proxyHost, proxyPort)           // 设置快代理的主机和端口            .withProxyAuth(proxyUsername, proxyPassword);  // 设置代理认证信息                WebDriver driver = null;        try {            // 创建Edge浏览器实例,也可以选择Chrome或Firefox            log.info("正在初始化WebDriver...");            driver = factory.createWebDriver(WebDriverFactory.BrowserType.EDGE);                        // 开始爬虫任务            crawlWebsite(driver);                    } catch (Exception e) {            // 异常处理,记录详细错误信息便于排错            log.error("爬虫执行出错: {}", e.getMessage(), e);        } finally {            // 确保WebDriver正确关闭,避免资源泄露            if (driver != null) {                driver.quit();                log.info("WebDriver已关闭,爬虫任务结束");            }        }    }        /**     * 爬虫核心逻辑,可根据实际需求扩展     *      * @param driver 已配置好的WebDriver实例     * @throws InterruptedException 如果线程休眠被中断     */    private static void crawlWebsite(WebDriver driver) throws InterruptedException {        // 访问目标网站        log.info("开始访问目标网站");        driver.get("https://www.baidu.com");        log.info("网页标题: {}", driver.getTitle());                // 显式等待某个元素出现,确保页面加载完成        // 比简单的Thread.sleep更智能        WebDriverWait wait = new WebDriverWait(driver, 10);        wait.until(ExpectedConditions.presenceOfElementLocated(By.tagName("body")));                // 获取页面内容示例:提取所有链接        log.info("开始提取页面链接");        List<WebElement> links = driver.findElements(By.tagName("a"));        log.info("共发现{}个链接", links.size());                // 处理提取到的链接        for (WebElement link : links) {            String text = link.getText().trim();            String href = link.getAttribute("href");            // 只记录有效链接            if (href != null && !href.isEmpty()) {                log.info("链接: {} -> {}", text.isEmpty() ? "[无文本]" : text, href);            }        }                // 模拟更多爬虫操作,例如点击某个元素、填写表单等        // 这里作为示例,只是简单等待        log.info("等待页面进一步处理...");        Thread.sleep(2000);                // 如果需要,可以继续访问更多页面        // driver.get("https://www.another-site.com");        // ...                log.info("爬虫任务完成");    }}4.3 配置代理的注意事项在使用代理时,需要注意以下几点:选择合适的代理类型:隧道代理适合大规模爬虫,普通代理适合小规模测试正确配置认证信息:确保用户名和密码正确,特殊字符需要URL编码测试代理连通性:使用前先测试代理是否可用合理设置请求频率:遵循代理服务商的使用建议,避免触发反爬机制注意IP切换时机:适时切换IP,避免同一IP频繁访问目标网站六、总结与展望本文详细介绍了如何使用Java+Selenium+快代理实现高效的网页爬虫。通过工厂模式和构建器模式的应用,我们实现了一个灵活、可扩展且易于使用的爬虫框架。该框架解决了代理认证配置的难题,优化了浏览器参数设置,提高了爬虫的稳定性和效率。Selenium与代理服务的结合为我们提供了强大的爬虫能力:Selenium模拟真实用户行为应对JavaScript渲染和复杂交互,而快代理则提供了稳定的IP资源池,有效规避IP封禁和地域限制问题。这种组合特别适合需要处理登录验证、动态加载内容或有反爬措施的网站。在实际应用中,请务必遵守相关法律法规和网站的使用条款,合理设置爬虫的请求频率和数量,避免对目标网站造成不必要的负担。同时,定期更新Selenium和WebDriver版本,以适应浏览器的更新和网站的变化。如果你在使用过程中遇到问题,可以参考快代理或查阅Selenium的相关资料。希望本文对你的爬虫开发有所帮助!最后,随着网站反爬技术的不断进化,爬虫技术也需要持续更新迭代。未来,我们可以考虑结合机器学习技术识别验证码,或通过更智能的策略调整爬取行为,使爬虫更加智能和高效。欢迎在评论区分享你的使用经验和改进建议————————————————原文链接:https://blog.csdn.net/weixin_66401877/article/details/147825058
  • [技术干货] Spring MVC 详解
    Spring MVC 基本概念1. Spring MVC 概述Spring MVC 是 Spring 框架中的一个模块,专注于为 Web 应用程序提供 Model-View-Controller (MVC) 架构。它帮助开发者构建可扩展、可维护的 Web 应用,并且能够轻松集成到 Spring 生态系统中。2. DispatcherServletDispatcherServlet 是 Spring MVC 的核心组件,负责接收 HTTP 请求,并将请求分发给相应的处理器(Controller)。它起到了中央控制器的作用。示例:@Configurationpublic class WebAppInitializer implements WebApplicationInitializer {    @Override    public void onStartup(ServletContext servletContext) throws ServletException {        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();        context.register(AppConfig.class);                ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(context));        dispatcher.setLoadOnStartup(1);        dispatcher.addMapping("/");    }}在这个例子中,DispatcherServlet 被注册到 ServletContext 中,并映射到根路径。3. ControllerController 是 Spring MVC 中的一个组件,负责处理用户请求,并返回一个 ModelAndView 对象。它将用户的输入与应用程序的业务逻辑连接起来。示例:import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.servlet.ModelAndView;@Controllerpublic class HomeController {    @GetMapping("/")    public ModelAndView home() {        ModelAndView mav = new ModelAndView("home");        mav.addObject("message", "Welcome to Spring MVC");        return mav;    }}在这个例子中,HomeController 处理根路径的 GET 请求,并返回一个包含消息的视图。4. ModelAndViewModelAndView 是 Spring MVC 中的一个类,包含了视图名称和模型数据。它将控制器的处理结果传递给视图层。示例:ModelAndView mav = new ModelAndView("home");mav.addObject("message", "Welcome to Spring MVC");在这个例子中,ModelAndView 对象包含视图名称 “home” 和模型数据 “message”。5. @RequestMapping@RequestMapping 是 Spring MVC 中的一个注解,用于映射 HTTP 请求到控制器的方法。它支持多种 HTTP 方法(GET、POST 等),还可以指定路径和参数。示例:import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;@Controllerpublic class HomeController {    @RequestMapping(value = "/", method = RequestMethod.GET)    public String home() {        return "home";    }}这个例子展示了如何使用 @RequestMapping 将根路径的 GET 请求映射到 home() 方法。6. @GetMapping 和 @PostMapping@GetMapping 和 @PostMapping 是 @RequestMapping 的快捷方式,分别用于处理 GET 和 POST 请求。示例:@GetMapping("/hello")public String hello() {    return "hello";}@PostMapping("/submit")public String submit() {    return "submit";}在这个例子中,@GetMapping 处理 GET 请求,@PostMapping 处理 POST 请求。7. @PathVariable@PathVariable 注解用于将 URL 路径中的变量绑定到方法参数上。示例:@GetMapping("/user/{id}")public String getUserById(@PathVariable("id") String userId) {    return "User ID: " + userId;}在这个例子中,@PathVariable(“id”) 将 URL 中的 {id} 绑定到方法参数 userId。8. @RequestParam@RequestParam 注解用于将查询参数绑定到方法参数上。示例:@GetMapping("/search")public String search(@RequestParam("q") String query) {    return "Search query: " + query;}在这个例子中,@RequestParam(“q”) 将查询参数 q 绑定到方法参数 query。9. @ModelAttribute@ModelAttribute 注解用于将请求中的数据绑定到模型对象上,通常用于表单处理。示例:@PostMapping("/submit")public String submitForm(@ModelAttribute User user) {    return "Form submitted for user: " + user.getName();}在这个例子中,@ModelAttribute 将请求中的表单数据绑定到 User 对象上。10. 视图解析器(View Resolver)视图解析器负责将逻辑视图名称解析为物理视图路径。Spring MVC 提供了多种视图解析器,如 InternalResourceViewResolver 和 ThymeleafViewResolver。示例:@Beanpublic InternalResourceViewResolver viewResolver() {    InternalResourceViewResolver resolver = new InternalResourceViewResolver();    resolver.setPrefix("/WEB-INF/views/");    resolver.setSuffix(".jsp");    return resolver;}在这个例子中,InternalResourceViewResolver 将视图名称解析为 JSP 文件路径。11. 表单处理Spring MVC 提供了对表单处理的全面支持,包括表单绑定、验证和表单回显。示例:import javax.validation.Valid;import org.springframework.validation.BindingResult;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.PostMapping;@PostMapping("/register")public String register(@Valid @ModelAttribute User user, BindingResult result) {    if (result.hasErrors()) {        return "register";    }    return "success";}在这个例子中,@Valid 注解用于验证 User 对象,BindingResult 用于处理验证结果。12. 数据验证(Validation)Spring MVC 支持基于 JSR-303 的数据验证,可以使用注解对模型对象进行验证。示例:import javax.validation.constraints.NotEmpty;public class User {    @NotEmpty(message = "Name is required")    private String name;    // getter 和 setter 方法}在这个例子中,@NotEmpty 注解用于验证 name 字段不能为空。13. 消息转换器(Message Converters)消息转换器负责将请求数据转换为对象,以及将对象转换为响应数据。Spring MVC 提供了多种内置的消息转换器,如 JSON 和 XML。示例:import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.ResponseBody;@PostMapping("/user")@ResponseBodypublic User createUser(@RequestBody User user) {    // 处理用户数据    return user;}在这个例子中,@RequestBody 和 @ResponseBody 注解用于将 JSON 请求数据转换为 User 对象,并将 User 对象转换为 JSON 响应数据。14. 拦截器(Interceptor)拦截器用于在请求处理之前或之后执行额外的逻辑。它们类似于过滤器,但更强大,可以访问 Spring MVC 上下文。示例:import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class MyInterceptor implements HandlerInterceptor {    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        // 执行前置逻辑        return true;    }}在这个例子中,MyInterceptor 实现了 HandlerInterceptor 接口,可以在请求处理之前执行逻辑。15. 异常处理(Exception Handling)Spring MVC 提供了多种异常处理机制,包括 @ExceptionHandler、@ControllerAdvice 和 ResponseEntityExceptionHandler。示例:import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvicepublic class GlobalExceptionHandler {    @ExceptionHandler(Exception.class)    public String handleException(Exception e) {        return "An error occurred: " + e.getMessage();    }}在这个例子中,@ExceptionHandler 注解用于处理控制器中的异常,@RestControllerAdvice 注解用于全局异常处理。16. 文件上传Spring MVC 支持文件上传功能,可以处理 Multipart 文件上传请求。示例:import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.multipart.MultipartFile;@PostMapping("/upload")public String uploadFile(@RequestParam("file") MultipartFile file) {    if (!file.isEmpty()) {        // 处理文件        return "File uploaded successfully";    }    return "File upload failed";}在这个例子中,@RequestParam 注解用于将上传的文件绑定到 MultipartFile 对象上。17.RestController@RestController 是 @Controller 和 @ResponseBody 的组合注解,通常用于构建 RESTful Web 服务。示例:import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class ApiController {    @GetMapping("/api/hello")    public String hello() {        return "Hello, RESTful world!";    }}在这个例子中,@RestController 注解用于构建一个 RESTful API。18. 国际化(Internationalization, i18n)Spring MVC 提供了对国际化的全面支持,允许根据用户的区域设置提供不同的语言和格式。示例:import org.springframework.context.annotation.Bean;import org.springframework.context.support.ResourceBundleMessageSource;@Beanpublic ResourceBundleMessageSource messageSource() {    ResourceBundleMessageSource source = new ResourceBundleMessageSource();    source.setBasename("messages");    return source;}在这个例子中,ResourceBundleMessageSource 用于加载国际化资源文件。19. Spring MVC 与 Thymeleaf 集成Thymeleaf 是一种流行的模板引擎,可以与 Spring MVC 集成,用于生成动态的 HTML 页面。示例:import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;@Controllerpublic class ThymeleafController {    @GetMapping("/greeting")    public String greeting(Model model) {        model.addAttribute("message", "Hello, Thymeleaf!");        return "greeting";    }}在这个例子中,ThymeleafController 处理 /greeting 请求,并返回一个包含消息的 Thymeleaf 模板视图。20. WebSocket 支持Spring MVC 提供了对 WebSocket 的支持,允许在 Web 应用程序中实现实时双向通信。示例:import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.config.annotation.EnableWebSocket;import org.springframework.web.socket.config.annotation.WebSocketConfigurer;import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;@Configuration@EnableWebSocketpublic class WebSocketConfig implements WebSocketConfigurer {    @Override    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {        registry.addHandler(new MyWebSocketHandler(), "/ws");    }}AI写代码在这个例子中,WebSocketConfig 配置了一个 WebSocket 处理器,用于处理 /ws 路径的 WebSocket 连接。总结Spring MVC 是一个功能强大的框架,提供了广泛的工具和特性来构建现代 Web 应用程序。通过理解和掌握上述每一个知识点,开发者可以更高效地构建出可扩展、维护性强的 Web 应用。————————————————原文链接:https://blog.csdn.net/weixin_37559642/article/details/142127152
  • [技术干货] js原型与原型链详解
    作为一个初入前端的小白,原型链的概念太多,一直觉得难以理解,对整个原型链的了解非常模糊。理解原型链是深入学习js的一小步,在参考诸多大佬文章后,整理笔记如下:        一,原型        原型与原型链,首先我们要知道什么是原型。在开始了解原型之前,卖个关子先认识下js中的对象    1, 对象        对象(Object)是一种复合数据类型,它是一种无序的键值对集合。对象用于存储和传递多个值,每个值都有一个键(key)与之关联。        对象的键是字符串类型,值可以为任意数据类型(数字,字符串,布尔)和其他对象。总之就是我们经常用到的 键值对啦  {key: value}    (对象又分为函数对象与普通对象,此处不赘述,以免绕晕,放到【 第五点 进阶 】下面)        对象的创建方式  以下是几种常见的对象创建方式:第一种: 对象字面量方式var obj1 = {  name: "Jack",  age: 26,}  第二种: Object构造函数模式var obj2 = new Object()obj2.name = "Jack"obj2.age = 26   第三种: 构造函数模式function Test(name, age){    this.name = name    this.age = age    this.say = function(){        console.log('我能说话')    }}var obj3 = new Test('Jack', 26)var obj4 = new Test('Rose', 25)          日常中我们最常用的应该是字面量方式,那为什么会出现第三种构造函数方式呢?        想想看我们需要多个obj1,obj2的时候,字面量方式我们需要重复代码去创建对象;而使用构造函数的方式只需要写一遍属性和方法,我们就可以通过new关键字,new出多个不同的对象。        试试在控制台打印如下obj3.say === obj4.say,false  看起来调用的是同一个函数方法,实际并不相等。因为他们的内存地址是不同的,每个new出来的obj3 和 obj4 都包含一份独立的属性和方法(可能导致浪费内存)function Test(){    this.name = 'rose'    this.say = function(){        console.log('我能说话')    }}var obj3 = new Test()var obj4 = new Test() obj3.say === obj4.say // falseobj3.name === obj4.name // true        你可能会问为什么obj3.name和obj4.name是相等的呢?刚才不是说的内存不同独立的属性和方法吗? 要理解这个行为,可以大致参考下面这种情况:const a = { value: 10 };const b = { value: 10 }; console.log(a.value === b.value);  // 输出 true,因为属性值相同console.log(a === b);              // 输出 false,因为是不同的对象        同样的逻辑,对比obj3.name的时候比较的是name这个属性值,而obj3.name输出值为rose,所以rose比较值obj4.name也是rose,是相同的        现在我们来说说构造函数方式,上面例子中的 obj3 和 obj4 都是 Test的实例(也叫实例对象),而Test是 obj3 和 obj4 的构造函数。 实例都有一个构造函数属性(constructor)指向构造函数,通过构造函数创建的对象拥有构造函数内部定义的属性和方法function Test(name, age){    this.name = name    this.age = age} Test.prototype.say = function(){    console.log('我能说话')}var obj3 = new Test('Jack', 26)var obj4 = new Test('Rose', 25) // constructor属性指向构造函数console.log(obj3.constructor === Test)  // trueconsole.log(obj4.constructor === Test)  // trueconsole.log(obj4.constructor === obj3.constructor) // true        记住以下两个概念:        (1)Test是构造函数        (2)obj3 和 obj4 是构造函数Test的 实例,实例的属性constructor指向构造函数Test    2,原型(原型对象)        上面的例子中, obj3 和 obj4 都需要调用 Test 中的say()方法,我们有没有办法将公共方法放到一个公共的地方呢? 这时候有请公共的原型(prototype)登场        在js中,每一个对象(函数也是对象)都有一个特殊的属性叫做原型(prototype),它指向另一个对象,这个对象(Test.prototype)被称为原型对象, 原型对象是用来共享属性和方法的        Test.prototype 就叫原型对象                打印Test.prototype可以看到上图中原型对象存在一个constructor属性,指向TestTest.prototype.constructor === Test// true        原型对象:        (1),原型对象有一个constructor属性指向构造函数本身(Test)。        (2),原型对象是一个普通的对象,它包含属性和方法。        (3),原型对象的属性和方法会被继承到所有通过原型链与它相连的对象。        简单来说,原型对象是一个普通对象,属性和方法会被继承到其他对象,而每个对象都有一个原型(prototype),用来继承其他对象的属性和方法。        此时,我们就可以把say方法放到这个原型对象上, obj3 和 obj4 就可以访问这个方法,再不用写到Test中去重复占用内存,所有new出来的实例都可以使用此方法        我们再来打印 obj3.say === obj4.say,为true, 证明obj3和obj4调用的就是同一个方法function Test(name, age){    this.name = name    this.age = age} Test.prototype.say = function(){    console.log('我能说话')}var obj3 = new Test('Jack', 26)var obj4 = new Test('Rose', 25) obj3.say()    // 我能说话obj4.say()    // 我能说话console.log(obj3.say === obj4.say)    // true            构造函数和实例之间就初步构成了这样一个关系,如图:  二,隐式原型__proto__    1,__proto__        在js中,每个对象都有一个“ __proto__ ”属性(左右两边两个短下划线),这个__proto__就被称为隐式原型。(记住这点)        实例对象当然也是对象,也存在__proto__属性 console.log(obj3.__proto__ === Test.prototype)// true        打印以上obj3.__proto__ === Test.prototype结果为true,所以:        (1),每个js对象都有一个隐藏的原型对象属性__proto__,它指向创建它的构造函数的原型对象(Test.prototype)        (2),__proto__存在的意义在于为原型链查找提供方向,原型链查找靠的是__proto__,而不是prototype(画重点,后面要考!!!)        实例对象obj3通过__proto__指向了Test的原型对象(Test.prototype),如图:(Test.prototype.constructor :从Test先指向原型对象Test.prototype在.constructor指回Test,绕了一圈,图中就不列举)      2,考你一下        前面提到的几个概念理解清楚,再来看看下面的列子是否清楚function Test(name, age){    this.name = name    this.age = age} Test.prototype.say = function(){    console.log('我能说话')}var obj3 = new Test('Jack', 26)  1, 构造函数是? 实例是?2, obj3.constructor === Test   true or false?3, obj3.__proto__ === Test ?4, Test.prototype === obj3.__proto__ ?5, obj3.__proto__.constructor === Test ? // 1, Test  obj3  2,true  3,false  4,true  5,true三,原型链    1,Object.prototype        在上面第二点中,每个js对象都有一个隐藏的原型对象属性__proto__        那Test的原型对象Test.prototype会不会也有一个隐式原型__proto__呢? 控制台输出如下:        Test.prototype当然也存在一个属性__proto__,而这个Test.prototype.__proto__到底是谁呢?Test.prototype.__proto__ === Object.prototype// true          (1) Test.prototype的隐式原型(__proto__)就是Object.prototype          (2) 所有的对象,包括构造函数的原型对象,最终都继承自 Object.prototype,这是js原型链的顶点        Object.prototype是从哪里来的呢? 当然是由Object的属性prototype指向来的。Object.prototype同样也会存在属性 constructor指回Object(【目录 2,原型 原型对象】中提到)        此时的关系图:    2,链        在控制台打印Object.prototype,会发现 Object.prototype也是一个对象        既然它也是对象,它也存在隐式属性__proto__。想想看,如果Object.prototype.__proto__再去指向某个对象的原型(prototype),那整条线就显得无穷无尽,一直找下去        js代码在创建时我们的开发者当然考虑到了,Object.prototype作为原型链的顶端,位于原型链的最末端。因此,它不再有自己的原型,所以Object.prototype.__proto__ 指向null,表示原型链的终点        原型链的终点是null        Object.prototype.__proto__ === null        这个时候终于到达了终点,形成了这样一个关系图(一整个链接在一起):         每个对象都有一个原型(prototype),它指向另外一个对象,而指向的对象又存在属性(_proto_)指向另外一个对象。当我们访问对象(obj3)的属性时,会先在对象定义的属性中进行查找,没找到就会沿着__proto__一路向上查找,最终形成一个链式结构,这整个链式结构就叫做原型链        如果在原型链中找到了这个属性,就返回找到的属性值;如果整个原型链都没找到这个属性值,则返回 undefined,没找到方法直接报错(not a function)四,练习一下        到了这里应该对整个原型链有了自己的认知,其实只要记住以下几个概念,就可以试着自己画出整个关系图1,在js中,每一个对象(函数也是对象)都有一个特殊的属性叫做原型(prototype),它指向另一个对象,这个对象被称为原型对象, 原型对象是用来共享属性和方法的 2,对象有一个属性(__proto__)指向构造函数的原型对象,构造函数的原型对象也存在__proto__ 3,原型链的顶端是Object.prototype 4,原型链的终点是null         看看下面的题是否清楚:function Test(name, age){    this.name = name    this.age = age}Test.prototype.say = function(){    console.log('我能说话')}var obj3 = new Test('Jack', 26)var obj4 = new Test('Rose', 24) 1, Test.prototype === ( ) ?2, obj3.__proto__.__proto__ === ( ) ?3, obj3.__proto__ === obj4.__proto__ ?4, Test.prototype.__proto__ === ( ) ?5, obj4.__proto__.constructor === ( ) ?6, Object.prototype.__proto__ === ( ) ?7, obj3.say === obj4.say ?  // 1, obj3.__proto__ 或 obj4.__proto    2,Object.prototype    3, true (二者都由Test new出来,在原型链上都指向 Test.prototype)// 4, Object.prototype    5, Test    6, null (终点)    7,true (同问题3)        要是不清楚可以在结合关系图捋一捋五,进阶    1,普通对象与函数对象        在 js 中,有两种主要类型的对象:普通对象和函数对象。普通对象最常见,通过"{ }"创建的就是普通对象;通过new Function出来的就是函数对象(函数声明、函数表达式创建的为函数对象),我们可以用typeof来区分  (注意:这里函数声明式和表达式不要和对象字面量方式混淆)        f1,f2,f3都是函数对象, Object 也是函数对象, b1,2,3为普通对象;         简单理解,普通对象就是我们最长见的 { } 键值对; 函数对象通常包含了一个functionfunction f1(){} var f2 = function(){} var f3 = new Function('name') var b1 = new f1() var b2 = {name: 'Rose'} var b3 = new Object()  typeof f1    // 'function'typeof f2    //'function'typeof f3    //'function'typeof b1    //'object'typeof b2    //'object'typeof b3    //'object'typeof Object // 'Function'        这是再来看我们上面的例子就很清晰了, obj3 为普通对象, Test为函数对象 (不信F12控制台打开 typeof试试)function Test(name, age){    this.name = name    this.age = age}var obj3 = new Test('Jack', 26)        在上面【2 原型对象】我们提到过每一个对象(函数也是对象)都有一个特殊的属性叫做原型(prototype)   obj3是对象,但是它没有prototype的这个原型属性(不信控制台试试)        所以这话不够完整,只有 函数对象才具有 prototype 这个原型属性    2,原型链机制        在【二,隐式原型】中,提到过: __proto__存在的意义在于为原型链查找提供方向。 看你是不是忘记了吧        为什么会说提供查找方向呢,看看下面两个例子:        左边在构造函数Test的原型对象(Test.prototype)上定义了sex, Test.sex为undefined ( 注意是Test.sex 不是 obj.sex); 右边在Object.prototype上定义sex ,Test.sex 能获取到值;  为什么在Test.prototype上定义,Tset.sex不能通过原型链到Test.prototype上去找到sex属性;而定义到顶点Object.prototype上,又能通过原型链找到了        看了大佬的一些解答分析,定义到Test.prototype上的时候,Test.sex并没有通过原型链查找,而是检查Test自身是否定义该属性,没有所以是undefined。感觉解释会有点说不通,定义到Object.prototype,不也应该是Test自身检查,也会检查不到,但我们能输出值,证明的确是顺着原型链去查找到了Object.prototype上                           why ?         来看看ai对此的回答                 我们在左边例子中,Test本身并没有直接定义'sex'属性,所以查找失败返回undefined, 如果要到原型对象上查找 正确的方式应该是 Test.prototype.sex   这结论很好没任何问题,但是为毛右边的例子能获取到呢 ?不要给我岔开话题啊喂....        为了找到答案,我一度去翻遍全网,问遍ai,得到结论大概都是:" 原型链从实例对象开始查找,不是从构造函数开始查找,构造函数不具备相同的原型链机制"   这回答非常好,可是还是没解决我的问题啊!!!  不具备相同的原型链机制,为什么定义到Object.prototype上就能获取到了呢?  反复询问ai,就开始给我绕圈子了.......  费解        终究还是得靠自己  先捋一下        既然__proto__才是原型链查找的方向,同时对象都有__proto__这个属性,那构造函数Test是属于函数对象,函数对象也是对象 那是否Test也会存在__proto__这个属性呢? 在上面的原型链图中并没有指出这个属性  请往下看    3,Function的原型        在第二点【隐式原型__proto__】中,我们提到__proto__指向创建它的构造函数的原型对象。function Test(name, age){    this.name = name    this.age = age}var obj = new Test('Jack', 26) Test.__proto__ === Function.prototype  // true        (1)构造函数Test的隐式原型( __proto__)指向 Function.prototype, 函数对象的__proto__指向Function.prototype        至于为什么,js在设计时决定了构造函数本身是函数,当然也可以通过指向 Function.prototype来访问原型链上的属性和方法,让构造函数也能参与到原型链中来(虽然不建议通过构造函数访问)        Function会有原型(prototype),当然也有隐式原型(__proto__),打印这两个原型,会发现二者互相相等  Function.prototype === Function.__proto__  是不是很神奇? 看起来像是自己创造了自己        针对 Function.prototype === Function.__proto__, 诸多大佬对此有各种不同的解释,以下抛出两个观点仅供参考:         1,Function 也是对象, 由new Function创建,所以自己创造自己                2,Function作为一个内置对象,代码运行前就存在,并非自己创造自己,先有的Function,然后实现上把原型指向了Function.prototype  以此来保持和其他函数一致,表明一种关系          对象都拥有隐式原型(__proto__),Function.prototype当然也存在__proto__,打印出来看看          好像有点眼熟? 是不是和Object.prototype打印的有点像  Function.prototype是函数对象,按照刚得出的结论,函数对象的__proto__应该指向Function.prototype(即Function.prototype.__proto === Function.prototype),但是自己指向自己并没有意义。 别忘记Object.prototype才是原型链的顶点,Function.prototype存在于原型链中必然会与Object.prototype存在关联,指向Object.prototype能保证原型链存在终点,所以Function.prototype.__proto__ === Object.prototype         再来看原型链关系,这时候就成了这样:        (2)如果在深究一点,Object也是函数对象,Object.__proto__ 也会指向Function.prototype        (3)构造函数Test也有constructor属性,这个属性指向创建该函数的构造函数;如果自己没有定义构造函数,会指向到 Function (Test.constructor === Function)         原型链关系就成了这样:        针对上面 【2,原型链机制】 中的问题也有了答案,在构造函数Test上访问Object.prototype中的属性时,其实是顺着Test.__proto__这条路径从Function去访问    空口无凭,证据如下,看看会输出什么function Test(name, age){    this.name = name    this.age = age}var obj = new Test('Jack', 26) Object.prototype.price = 2000 Function.prototype.price = 300 Test.price        Test在自身没有找到price,顺着Test的__proto__到Function.prototype上找到了price = 300,所以直接返回 300;若Function.prototype上没有price,才会进一步顺着__proto__找到Object.prototype         在实际使用中,要获取定义到Test.prototype上的属性,还可以用原型对象Test.prototype.price访问;不过建议还是通过实例(obj)来访问具体的属性(obj.name),而不是构造函数Test.name访问,毕竟实例new出来的目的就是为了调用构造函数上的方法属性        真相大白,再一次证明__proto__存在的意义在于为原型链查找提供方向,原型链查找靠的是__proto__,而不是prototype       六,练习一下        1,function Test(){}var obj1 = new Test() 1, console.log( obj1.__proto__ === Test.prototype )2, obj1.__proto__.__proto__ === ()?3, Object.prototype.__proto__ === ()?4, console.log( Test.prototype === obj1.__proto__ )5, Test.__proto__ === Function.prototype ?6, Function.prototype.__proto__ === () ?7, Funcion.prototype === Function.__proto__ ? // 1,true 2,Object.prototype 3,null 4,true 5,true 6,Object.prototype 7,true        2,var Test = function(){}Test.prototype.name = 'Rose'var obj1 = new Test()Test.prototype = {name: 'Rose', age: 26}var obj2 = new Test() obj1.nameobj1.ageobj2.nameobj2.age // 'Rose'  undefined  'Rose'  26// Test.prototype =  {} 将Test.prototype.name 覆盖        3,function Test(){} Object.prototype.a = function(){    console.log(1)} Function.prototype.b = function(){    console.log(2)} var obj = new Test() obj.a()obj.b()Test.a()Test.b() //  1  (not a function)  1  2// 不太清楚再去看看Function的原型七,总结        1,每个对象均存在隐式原型(__proto__),函数对象才有prototype属性        2,__proto__存在的意义在于为原型链查找提供方向,原型链查找靠的是__proto__,而不是prototype        3,函数对象的__proto__都指向Function.prototype        4,每个对象都有一个隐式原型属性(__proto__),多个原型通过__proto__链接在一起形成的链式结构就是原型链————————————————原文链接:https://blog.csdn.net/Yi_qian1000/article/details/135264247
  • [技术干货] 基于Java和高德开放平台的WebAPI集成实践-以搜索POI2.0为例
    前言        在当今数字化时代,地理信息系统(GIS)和位置服务(LBS)已成为许多应用程序的核心组成部分。无论是导航、物流、社交网络还是电子商务,位置数据的获取和处理都显得尤为重要。高德开放平台作为国内领先的地理信息服务提供商,提供了丰富的WebAPI接口,帮助开发者快速集成地图、导航、搜索等功能。其中,POI(Point of Interest)搜索是许多应用场景中的关键功能,它能够帮助用户快速找到附近的兴趣点,如餐馆、酒店、加油站等。        Java作为一种广泛使用的编程语言,因其跨平台性、稳定性和丰富的生态系统,成为许多企业级应用的首选开发语言。将Java与高德开放平台的WebAPI进行集成,不仅可以充分利用Java的强大功能,还能快速实现复杂的地理信息服务。本文将以搜索POI2.0为例,详细介绍如何在Java项目中集成高德开放平台的WebAPI,并实现高效的POI搜索功能。示例将涵盖从API密钥的获取、HTTP请求的发送、数据的解析到最终结果的展示等完整流程。通过本文的学习,开发者将能够掌握Java与高德开放平台WebAPI集成的基本方法,并能够根据实际需求,灵活应用这些技术解决实际问题。        总之,Java与高德开放平台的WebAPI集成,为开发者提供了强大的工具,帮助他们快速构建高效、稳定的地理信息服务应用。通过本文的实践,开发者将能够更好地理解和使用高德开放平台的API,为未来的项目开发打下坚实的基础。一、高德搜索API简介        介绍高德开放平台的功能和主要服务,包括地图、定位、导航等API的概述。其次介绍如何在高德平台中申请个人的账号,以及简要介绍对接方法。1、高德开放平台        在之前的系列博客中,我们介绍了天地图的相关WebAPI,基于天地图的WebAPI我们可以做一些应用。由于一些客观存在的原因,比如天地图的一些POI数据,与百度和高德等平台还是有一定差距,下次可以针对某一个具体的检索场景,对同一个区域进行检索,来对比一下三个平台的数据情况,本文暂且不表。因此为了能有更精准的数据参与分析,这里我们来说说如何从高德平台获取数据。进入到开放平台的首页后可以看到以下界面,这里有很多介绍的文字,大家可以到官网看一下,不再赘述。        点击界面中的产品介绍,进入详细的产品介绍页面,操作界面如下:         这里点击搜索进入到具体的搜索专题介绍页面,地图搜索与产品构建息息相关,基于地理空间的搜索结果,可以灵活、直观地呈现在地图图面上,为用户带来全新价值。目前我们的搜索服务包括:POI搜索、地理/逆地理编码、输入提示、天气及行政区域查询。2、搜索功能介绍         平台的POI搜索API提供了多种搜索方式,包括关键字搜索、周边搜索、多边形搜索等。关键字搜索:开发者可通过文本关键字搜索地点信息,文本可以是结构化地址,例如:北京市朝阳区望京阜荣街10号;也可以是 POI 名称,例如:首开广场;周边搜索:开发者可设置圆心和半径,搜索圆形区域内的地点信息;多边形区域搜索:开发者可设置首尾连接的几何点组成多边形区域,搜索坐标对应多边形内的地点信息;ID搜索:开发者可通过已知的地点 ID(POI ID)搜索对应地点信息,建议结合输入提示接口使用。        通过这些API,开发者可以根据用户的需求,灵活地获取所需的POI信息。POI搜索2.0版本在原有功能的基础上,进一步优化了搜索算法,提升了搜索结果的准确性和响应速度。同时,API还支持多种数据格式的返回,如JSON、XML等,方便开发者根据项目需求进行数据解析和处理。3、部分API介绍        关于开放平台接口的API介绍,官方有详细的说明,这里我们以按关键字搜索为例子,详细介绍它的请求参数以及响应参数信息。        关键字搜索 API 服务地址URL请求方式https://restapi.amap.com/v5/place/text?parametersGET        parameters 代表的参数包括必填参数和可选参数。所有参数均使用和号字符(&)进行分隔。下面的列表枚举了这些参数及其使用规则。         请求参数参数名含义规则说明是否必须缺省值key高德Key用户在高德地图官网 申请 Web 服务 API 类型Key必填无keywords地点关键字需要被检索的地点文本信息。只支持一个关键字 ,文本总长度不可超过80字符必填(keyword 或者 types 二选一必填)无types指定地点类型地点文本搜索接口支持按照设定的 POI 类型限定地点搜索结果;地点类型与 poi typecode 是同类内容,可以传入多个 poi typecode,相互之间用“|”分隔,内容可以参考 POI 分类码表;地点(POI)列表的排序会按照高德搜索能力进行综合权重排序;可选(keyword 或者 types 二选一必填)120000(商务住宅)150000(交通设施服务)region搜索区划增加指定区域内数据召回权重,如需严格限制召回数据在区域内,请搭配使用 city_limit 参数,可输入 citycode,adcode,cityname;cityname 仅支持城市级别和中文,如“北京市”。可选无,默认全国范围内搜索city_limit指定城市数据召回限制可选值:true/false为 true 时,仅召回 region 对应区域内数据。可选falseshow_fields返回结果控制show_fields 用来筛选 response 结果中可选字段。show_fields 的使用需要遵循如下规则:1、具体可指定返回的字段类请见下方返回结果说明中的“show_fields”内字段类型;2、多个字段间采用“,”进行分割;3、show_fields 未设置时,只返回基础信息类内字段。可选空page_size当前分页展示的数据条数page_size 的取值1-25可选page_size 默认为10page_num请求第几分页请求第几分页可选page_num 默认为1sig数字签名请参考 数字签名获取和使用方法可选无output返回结果格式类型默认格式为 json,目前只支持 json 格式;可选jsoncallback回调函数callback 值是用户定义的函数名称,此参数只在 output 参数设置为 JSON 时有效。可选无        返回结果名称类型说明statusstring本次 API 访问状态,如果成功返回1,如果失败返回0。infostring访问状态值的说明,如果成功返回"ok",失败返回错误原因,具体见 错误码说明。infocodestring返回状态说明,10000代表正确,详情参阅info状态表countstring单次请求返回的实际 poi 点的个数poisobject返回的 poi 完整集合poi单个 poi 内包含的完整返回数据namestringpoi 名称idstringpoi 唯一标识locationstringpoi 经纬度typestringpoi 所属类型typecodestringpoi 分类编码pnamestringpoi 所属省份citynamestringpoi 所属城市adnamestringpoi 所属区县addressstringpoi 详细地址pcodestringpoi 所属省份编码adcodestringpoi 所属区域编码citycodestringpoi 所属城市编码注意以下字段如需返回需要通过“show_fields”进行参数类设置。childrenobject设置后返回子 POI 信息idstring子 poi 唯一标识namestring子 poi 名称locationstring子 poi 经纬度addressstring子 poi 详细地址subtypestring子 poi 所属类型typecodestring子 poi 分类编码snamestring子 poi 分类信息subtypestring再次确认子 poi 分类信息businessobject设置后返回 poi 商业信息business_areastringpoi 所属商圈opentime_todaystringpoi 今日营业时间,如 08:30-17:30 08:30-09:00 12:00-13:30 09:00-13:00opentime_weekstringpoi 营业时间描述,如 周一至周五:08:30-17:30(延时服务时间:08:30-09:00;12:00-13:30);周六延时服务时间:09:00-13:00(法定节假日除外)telstringpoi 的联系电话tagstringpoi 特色内容,目前仅在美食poi下返回ratingstringpoi 评分,目前仅在餐饮、酒店、景点、影院类 POI 下返回coststringpoi 人均消费,目前仅在餐饮、酒店、景点、影院类 POI 下返回parking_typestring停车场类型(地下、地面、路边),目前仅在停车场类 POI 下返回aliasstringpoi 的别名,无别名时不返回keytagstringpoi 标识,用于确认poi信息类型 rectagstring用于再次确认信息类型 indoorobject设置后返回室内相关信息indoor_mapstring是否有室内地图标志,1为有,0为没有cpidstring如果当前 POI 为建筑物类 POI,则 cpid 为自身 POI ID;如果当前 POI 为商铺类 POI,则 cpid 为其所在建筑物的 POI ID。indoor_map 为0时不返回floorstring楼层索引,一般会用数字表示,例如8;indoor_map 为0时不返回truefloorstring所在楼层,一般会带有字母,例如F8;indoor_map 为0时不返回naviobject设置后返回导航位置相关信息navi_poiidstringpoi 对应的导航引导点坐标。大型面状 POI 的导航引导点,一般为各类出入口,方便结合导航、路线规划等服务使用entr_locationstringpoi 的入口经纬度坐标exit_locationstringpoi 的出口经纬度坐标gridcodestringpoi 的地理格 idphotosobject设置后返回 poi 图片相关信息titlestringpoi 的图片介绍urlstringpoi 图片的下载链接二、Uniapi集成高德API        本节详细介绍如何在Java中使用Uniapi来集成高德api实现相关的检索服务。主要从以下三个部分进行介绍,第一是介绍API的集成流程;第二是介绍如何在Uniapi中定义接口;第三是介绍如何在业务中进行集成。1、API集成流程        在Java项目中集成高德开放平台的WebAPI,通常需要以下几个步骤:首先,开发者需要在高德开放平台注册账号,并创建应用以获取API密钥(Key)。API密钥是调用高德API的凭证,每个应用都有唯一的密钥。其次,开发者需要在Java项目中引入HTTP客户端库,用于发送HTTP请求并接收API的响应。常用的HTTP客户端库包括Apache HttpClient、OkHttp、Uniapi等,本文将以Uniapi为例重点介绍。接下来,开发者需要根据高德API的文档,构建符合要求的请求URL,并处理API返回的数据。最后,开发者可以根据业务需求,对获取的POI数据进行进一步的处理和展示。关于如何在高德开放平台中申请账号并获取APIkey,大家可以在官方文档中查找,不再做详细的介绍。2、访问接口的定义        这里介绍如何在Uniapi中创建访问api,用来跟开放平台进行交互,uniapi的操作比较简单,下面是示例代码:package com.yelang.project.thridinterface;import com.burukeyou.uniapi.http.annotation.HttpApi;import com.burukeyou.uniapi.http.annotation.param.QueryPar;import com.burukeyou.uniapi.http.annotation.request.GetHttpInterface;import com.burukeyou.uniapi.http.core.response.HttpResponse; @HttpApi(url = "https://restapi.amap.com/v5")public interface AmapSearchService {@GetHttpInterface("/place/text")public HttpResponse<String> getSearch(@QueryPar("keywords") String keywords,@QueryPar("types") String types,@QueryPar("region") String region,@QueryPar("page_size") String page_size,@QueryPar("page_num") String page_num ,@QueryPar("show_fields")String show_fields, @QueryPar("key") String key); @GetHttpInterface("/place/polygon")public HttpResponse<String> searchByPolygon(@QueryPar("polygon") String polygon,@QueryPar("keywords") String keywords,@QueryPar("types") String types,@QueryPar("page_size") String page_size,@QueryPar("page_num") String page_num ,@QueryPar("show_fields")String show_fields, @QueryPar("key") String key);}        在上面的例子中,创建了两个访问接口。第一个方法是根据关键字来调用检索,第二个 方法是根据一个polygon范围来进行检索。上述接口中的参数说明已经在第一节中进行讲解,可以往前搜索以下。3、业务调用集成        接下来讲解如何在Java当中调用Uniapi定义的接口,根据我们传入的参数来查询目标POI。比如我们需要查询湖南省长沙市岳麓区(代码:430104)的餐饮服务(pot type:050000)的数据,每页数据返回的大小为20条数据。集成的访问代码如下:package com.yelang.project.unihttp;import org.geotools.geometry.jts.JTSFactoryFinder;import org.junit.Test;import org.junit.runner.RunWith;import org.locationtech.jts.geom.Coordinate;import org.locationtech.jts.geom.GeometryFactory;import org.locationtech.jts.geom.LinearRing;import org.locationtech.jts.geom.Polygon;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import com.burukeyou.uniapi.http.core.response.HttpResponse;import com.google.gson.Gson;import com.yelang.common.utils.StringUtils;import com.yelang.common.utils.geo.CoordinateTransformUtil;import com.yelang.project.education.domain.amap.AmapSearchVO;import com.yelang.project.thridinterface.AmapSearchService;@SpringBootTest@RunWith(SpringRunner.class)public class AmaPOISearchCase { private static final String AMAP_CLIENT_AK = "申请的访问key";@Autowiredprivate AmapSearchService amapSearchService;/*** - 关键字搜索 API 服务地址* @throws InterruptedException */@Testpublic void searchByKeyWordOrTypies() throws InterruptedException {String keywords = "";String types = "050000";String page_size = "20";String region = "430104";String show_fields = "children,business,indoor,navi,photos";HttpResponse<String> result = null;for(int i = 1;i<= 1;i++) {result = amapSearchService.getSearch(keywords, types,region, page_size,String.valueOf(i),show_fields,AMAP_CLIENT_AK);System.out.println(result.getBodyResult());Thread.sleep(3000L);//休眠3秒}}}        接下来我们运行上面的程序,可以在控制台中看到以下的输出:        看到以下的信息说明返回成功,接口成功调用,下面我们将返回结果进行格式化。三、常见问题与优化        在实际开发过程中,开发者可能会遇到一些常见问题,如API调用频率限制、数据解析错误、网络请求超时等。针对这些问题,开发者可以通过合理的代码设计和异常处理机制,确保系统的稳定性和可靠性。此外,高德开放平台还提供了详细的开发文档和技术支持,帮助开发者快速解决遇到的问题。         在实际项目开发过程中,需要注意相关key的保护问题。这里使用的明文保存的方式,在使用高德API时需要注意的安全性问题,如API Key的保护、数据传输的加密等。可以采用加密的方式对明文进行保存,在访问时进行解密即可。四、总结        以上就是本文的主要内容,本文将以搜索POI2.0为例,详细介绍如何在Java项目中集成高德开放平台的WebAPI,并实现高效的POI搜索功能。示例将涵盖从API密钥的获取、HTTP请求的发送、数据的解析到最终结果的展示等完整流程。通过本文的学习,开发者将能够掌握Java与高德开放平台WebAPI集成的基本方法,并能够根据实际需求,灵活应用这些技术解决实际问题。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。————————————————原文链接:https://blog.csdn.net/yelangkingwuzuhu/article/details/147867639
  • [技术干货] java 基础知识
    1.概念1.1.java的特定有哪些?平台无关性:"一次编译终身运行",就是说java编译器会将源代码先编译成字节码(不同机器上只要源代码相同,那么就字节码一定相同),然后JVM会将字节码翻译成对应的机器码(JVM有不同系统的版本,JVM会根据你的环境进行翻译),总的来说就是字节码保证编译一致,然后根据不同的JVM翻译对应机器码面向对象:java是一个面向对象的语言,更方便代码的维护和使用内存管理:java拥有垃圾回收机制,开发人员不需要自己手动删除内存(不需要考虑这么多),由垃圾回收机制来自动实现内存释放1.2.java有哪些优势哪些劣势?优势:(特定那里)跨平台:java有平台无关性内存管理:拥有垃圾回收机制面向对象:使代码更好维护和使用强大的生态系统:如Spring全家桶,其他的工具包等等稳定性:支持企业长期使用,版本向后兼容劣势:内存消耗大:JVM虚拟机本身就需要一定的内存启动时间长:由于需要JVM将字节码翻译成机器码代码复杂:由于java过于面向对象,一个简单的程序的代码复杂(麻烦)1.3.java为什么可以跨平台?java在编译时,java编译器会直接将源代码编写成字节码文件,等到你需要运行时,会根据你的JVM虚拟机版本不同,然后通过不同的JVM将字节码翻译成机器码(让机器识别)1.4JVM,JDK,JRE它们有什么区别?JVM:JVM是java虚拟机,是java程序运行的环境JDK:JDK是java的一个开发工具包(各种类,工具)JRE:JRE是java运行的环境,是java程序所需的最小环境1.5.编译型语言与解释型语言的区别?编译型:在运行之前需要编译,将源代码编写成字节码或者机器码(C++),如果它编写成机器码,在本机上可以直接识别机器码,从而它的运行速度快,跨平台性差解释性:不需要编译,它会在运行时逐行解释代码,因运行速度慢,跨平台性好2.数据类型2.1.long与int类型可以互转吗?可以:但是你需要考虑数据的溢出与丢失比如:你将long类型转成int类型,如果你的long类型的数很大,大到int无法全部接收,那么就会出现数据的丢失比如:你将int类型转成long类型,如果你的int类型的数很小,那么用long接收,由于long精度大,其他位将用0填充,出现数据溢出public class TypeConversion {    public static void main(String[] args) {        long bigLong = 2147483648L; // 超出 int 最大值(2147483647)        int intValue = (int) bigLong; // 强制转换                System.out.println("原始 long 值: " + bigLong);  // 输出 2147483648        System.out.println("转换后的 int 值: " + intValue); // 输出 -2147483648(溢出)    }}2.2.数据类型转变形式有哪些?自动类型转换(隐式转换):假如:你使用long类型与int类型进行相加,它会默认将int类型隐式转换成long,再进行运算public class AutoConversion {    public static void main(String[] args) {        int a = 10;        long b = 20L;        long result = a + b;  // int 自动提升为 long                System.out.println(result);  // 输出 30    }}强制类型转换(显示转换):假如:你使用long类型与int类型进行相加,那么最后的结果只能用long类型接收,如果你想要使用int类型接收,那么需要强制类型转换public class ForceConversion {    public static void main(String[] args) {        long a = 2147483648L;  // 超过 int 最大值(2147483647)        int b = (int) a;       // 强制转换                System.out.println(b);  // 输出 -2147483648(高位截断)    }}字符串转换:使用包装类里面的方法进行转换,比如:你将char类型转换成int,那么它会根据ASCII码对应表转成对应数public class StringConversion {    public static void main(String[] args) {        // 1. 字符串 → 数值类型(需处理异常)        String str = "123";        int num = Integer.parseInt(str);  // 字符串转 int        double d = Double.parseDouble(str);  // 字符串转 double                // 2. 数值类型 → 字符串        String s1 = Integer.toString(num);  // 方法1        String s2 = String.valueOf(d);      // 方法2        String s3 = "" + num;              // 方法3(隐式转换)         // 3. char → int(ASCII 码转换)        char c = 'A';        int ascii = c;  // 直接赋值,输出 65        int numericValue = Character.getNumericValue('9');  // 输出 9    }}2.3.类型转换会出现哪些问题?数据丢失:小类型转大类型(精度不同,造成数据丢失)数据溢出:大类型转小类型(高位全用0填充)精度不同:float是单精度,double是双精度,两者转换精度会出现丢失类型不同:不同类型的转换会出现编译错误2.4.为什么用bigDecimal 不用double?举例:你能使用十进制表示1/3吗?无法表示,一直在循环,而double是进行二进制运算的,比如:0.1你能使用二进制进行表示吗?也是一直循环,而double是有精度的,到达它的精度限度后,它不会在循环下去,因此会出现精度丢失问题解决:使用bigDecimal(注意:如果你还是使用浮点数赋值给它,还是会出现该问题(默认浮点数就是double类型),因此使用字符串赋值)System.out.println(0.1 + 0.2); // 输出 0.30000000000000004(精度丢失)import java.math.BigDecimal; public class BadExample {    public static void main(String[] args) {        BigDecimal a = new BigDecimal(0.1); // 用 double 初始化(错误!)        BigDecimal b = new BigDecimal(0.2);        BigDecimal result = a.add(b);                System.out.println(result); // 输出 0.3000000000000000166533453694...    }}import java.math.BigDecimal; public class GoodExample {    public static void main(String[] args) {        BigDecimal a = new BigDecimal("0.1"); // 用字符串初始化(正确!)        BigDecimal b = new BigDecimal("0.2");        BigDecimal result = a.add(b);                System.out.println(result); // 输出 0.3    }}2.5.装箱与拆箱会出现什么问题?装箱:就是将基本类型包装成包装类拆箱:就是将包装类拆成基本类型问题:由于java可以实现自动装箱与拆箱,如果你定义一个Integer类型,但是没有给它赋值,它会默认null,然后将它拆箱,但是null值是无法拆箱的,因此会出现空指针异常2.6.Integer为什么会存在一个缓存池?由于我们需要使用对应的包装类,但是每次都需要重新创建对象(消耗内存),并且进行一个运算就会创建一个对象(无关内存),因此为了节省内存,java将我们常使用的-128到127的对象已经创建好了,放在静态缓存池中,你使用.valueInt()这个方法进行赋值,它就会去复用池中对象(地址相同)Integer a = Integer.valueOf(100);  // 从缓存池获取(地址相同)Integer b = 100;                   // 隐式调用 valueOf(),复用缓存Integer c = new Integer(100);      // 强制创建新对象(地址不同) System.out.println(a == b);  // true(同一对象)System.out.println(a == c);  // false(不同对象)3.面向对象3.1.怎么理解面向对象?面向对象就是将现实生活中的各个事务抽象成对象3.2.说一下封装继承多态?封装:就是将共性的属性与方法封装到类中,隐藏类中的细节,仅暴露出你想要暴露的接口来与外界交互,增加了代码的安全性和扩展性继承:子类共享父类的属性与方法,是代码实现复用,建立了类与类之间的联系,是类之间的结构更加清晰多态:不同类对应不同消息的不同操作方式,是代码更加灵活3.3.多态体现在哪几个方面?方法重载:同一个类可以有多个方法名相同的方法(参数不同)方法重写:子类重写父类的方法接口实现:不同的类实现同一个接口,那么创建对象时,可以使用接口创建,调用方式一致向上与向下转型:向上:父类转子类   向下:子类转父类3.4.重写与重载的区别?重写:子类重写父类方法,需要与父类方法名,返回类型,参数保持一致,只能修改其内部的代码重载:在同一个类中你可以重载多个方法名相同的方法,但是区别在于参数不同(1.参数类型不同,2.参数数量不同,3.参数顺序不同),满足一个即可3.5.抽象类与普通类的区别?实例化:抽象类不可以实例化,普通类可以-----方法实现:抽象类方法可以实现可以只定义,而普通类需要具体的方法实现3.6.抽象类与接口的区别?实现方式:抽象类需要继承extends,接口需要实现implements-----访问修饰符不同:两者的属性与方法默认修饰符不同-----方法实现不同:抽象类方法可以定义可以实现,接口只能定义-----变量:抽象类可以有实例变量和静态变量,而接口只有静态变量-----特点:接口是定义类的功能或行为,抽象类是描述类的共性和行为的3.7.抽象类可以加final关键字吗?不可以,抽象类本身就是一个基类就是让其他类继承的,而类加上final会让该类无法被继承因此两者互斥3.8.解释一下静态变量与静态方法?静态的东西只有当类加载完后,它就会加载,只会在内存中加载一次3.9.为什么静态不能调用非静态?静态只有当类加载完就会加载,因此静态会优先于非静态加载(需要实例化),你一个没加载的怎么能被调用呢?比如:静态方法里面调用非静态方法,我静态方法都已经加载完了,而你非静态方法必须要实例化才能加载,没有实例化不加载,那么我怎么调用你呢?(不能确定非静态是否加载)比如:反过来,就可以解释为什么非静态可以调用静态,因为非静态加载慢,它加载完就一定有静态加载完,因此可以调用public class Example {    static int staticVar = 10;  // 类加载时初始化    int instanceVar = 20;      // 对象实例化时初始化}public class Example {    static void staticMethod() {        instanceMethod();  // 编译错误:无法调用非静态方法        System.out.println(instanceVar);  // 编译错误:无法访问非静态变量    }     void instanceMethod() {        System.out.println("非静态方法");    }}public class Example {    static int staticVar = 10;     void instanceMethod() {        System.out.println(staticVar);  // 合法:静态成员已加载        staticMethod();                // 合法:静态方法已加载    }     static void staticMethod() {        System.out.println("静态方法");    }}3.10.为什么非静态内部类可以访问外部类?就是当外部类实例化后,会将外部类的实例化地址(引用)当作参数传给非静态内部类,因此它可以根据引用来访问4.关键字4.1.final修饰类    代表类不能被继承修饰方法    代表方法不能被重写修饰变量    如果是基本类型,值不能被修改,是引用类型,值可以被修改,地址不能修改————————————————原文链接:https://blog.csdn.net/2402_88700528/article/details/148147485
  • [技术干货] java基础语法
    Java基础语法一个Java程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来协同工作。下面简要介绍下类、对象、方法和实例变量的概念。对象:对象是类的一个实例,有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。类:类是一个模板,它描述一类对象的行为和状态。方法:方法就是行为,一个类可以有很多方法。逻辑运算、数据修改以及所有动作都是在方法中完成的。实例变量:每个对象都有独特的实例变量,对象的状态由这些实例变量的值决定。第一个Java程序下面看一个简单的Java程序,它将打印字符串 Hello Worldpublic class MyFirstJavaProgram {   /* 第一个Java程序.      * 它将打印字符串 Hello World    */    public static void main(String []args) {       System.out.println("Hello World"); // 打印 Hello World    }} 下面将逐步介绍如何保存、编译以及运行这个程序:打开Notepad,把上面的代码添加进去;把文件名保存为:MyFirstJavaProgram.java;打开cmd命令窗口,进入目标文件所在的位置,假设是C:\在命令行窗口键入 javac MyFirstJavaProgram.java  按下enter键编译代码。如果代码没有错误,cmd命令提示符会进入下一行。(假设环境变量都设置好了)。再键入java MyFirstJavaProgram 按下Enter键就可以运行程序了你将会在窗口看到 Hello WorldC : > javac MyFirstJavaProgram.javaC : > java MyFirstJavaProgram Hello World基本语法编写Java程序时,应注意以下几点:大小写敏感:Java是大小写敏感的,这就意味着标识符Hello与hello是不同的。类名:对于所有的类来说,类名的首字母应该大写。如果类名由若干单词组成,那么每个单词的首字母应该大写,例如 MyFirstJavaClass 。方法名:所有的方法名都应该以小写字母开头。如果方法名含有若干单词,则后面的每个单词首字母大写。源文件名:源文件名必须和类名相同。当保存文件的时候,你应该使用类名作为文件名保存(切记Java是大小写敏感的),文件名的后缀为.java。(如果文件名和类名不相同则会导致编译错误)。主方法入口:所有的Java 程序由public static void main(String args[])方法开始执行。Java标识符Java所有的组成部分都需要名字。类名、变量名以及方法名都被称为标识符。关于Java标识符,有以下几点需要注意:所有的标识符都应该以字母(A-Z或者a-z),美元符($)、或者下划线(_)开始首字符之后可以是任何字符的组合关键字不能用作标识符标识符是大小写敏感的合法标识符举例:age、$salary、_value、__1_value非法标识符举例:123abc、-salaryJava修饰符像其他语言一样,Java可以使用修饰符来修饰类中方法和属性。主要有两类修饰符:可访问修饰符 : default, public , protected, private不可访问修饰符 : final, abstract, strictfp在后面的章节中我们会深入讨论Java修饰符。Java变量Java中主要有如下几种类型的变量局部变量类变量(静态变量)成员变量(非静态变量)Java数组数组是储存在堆上的对象,可以保存多个同类型变量。在后面的章节中,我们将会学到如何声明、构造以及初始化一个数组。Java枚举Java 5.0引入了枚举,枚举限制变量只能是预先设定好的值。使用枚举可以减少代码中的bug。例如,我们为果汁店设计一个程序,它将限制果汁为小杯、中杯、大杯。这就意味着它不允许顾客点除了这三种尺寸外的果汁。实例class FreshJuice {   enum FreshJuiceSize{ SMALL, MEDUIM, LARGE }   FreshJuiceSize size;}public class FreshJuiceTest {   public static void main(String args[]){      FreshJuice juice = new FreshJuice();      juice.size = FreshJuice. FreshJuiceSize.MEDUIM ;   }}注意:枚举可以单独声明或者声明在类里面。方法、变量、构造函数也可以在枚举中定义。Java关键字下面列出了Java保留字。这些保留字不能用于常量、变量、和任何标识符的名称。关键字    描述abstract    抽象方法,抽象类的修饰符assert    断言条件是否满足boolean    布尔数据类型break    跳出循环或者label代码段byte    8-bit 有符号数据类型case    switch语句的一个条件catch    和try搭配扑捉异常信息char    16-bit Unicode字符数据类型class    定义类const    未使用continue    不执行循环体剩余部分default    switch语句中的默认分支do    循环语句,循环体至少会执行一次double    64-bit双精度浮点数else    if条件不成立时执行的分支enum    枚举类型extends    表示一个类是另一个类的子类final    表示一个值在初始化之后就不能再改变了表示方法不能被重写,或者一个类不能有子类finally    为了完成执行的代码而设计的,主要是为了程序的健壮性和完整性,无论有没有异常发生都执行代码。float    32-bit单精度浮点数for    for循环语句goto    未使用if    条件语句implements    表示一个类实现了接口import    导入类instanceof    测试一个对象是否是某个类的实例int    32位整型数interface    接口,一种抽象的类型,仅有方法和常量的定义long    64位整型数native    表示方法用非java代码实现new    分配新的类实例package    一系列相关类组成一个包private    表示私有字段,或者方法等,只能从类内部访问protected    表示字段只能通过类或者其子类访问子类或者在同一个包内的其他类public    表示共有属性或者方法return    方法返回值short    16位数字static    表示在类级别定义,所有实例共享的strictfp    浮点数比较使用严格的规则super    表示基类switch    选择语句synchronized    表示同一时间只能由一个线程访问的代码块this    表示调用当前实例或者调用另一个构造函数throw    抛出异常throws    定义方法可能抛出的异常transient    修饰不要序列化的字段try    表示代码块要做异常处理或者和finally配合表示是否抛出异常都执行finally中的代码void    标记方法不返回任何值volatile    标记字段可能会被多个线程同时访问,而不做同步while    while循环Java注释类似于C/C++,Java也支持单行以及多行注释。注释中的字符将被Java编译器忽略。public class MyFirstJavaProgram{   /* 这是第一个Java程序    *它将打印Hello World    * 这是一个多行注释的示例    */    public static void main(String []args){       // 这是单行注释的示例       /* 这个也是单行注释的示例 */       System.out.println("Hello World");     }} Java 空行空白行,或者有注释的的行,Java编译器都会忽略掉。继承在Java中,一个类可以由其他类派生。如果你要创建一个类,而且已经存在一个类具有你所需要的属性或方法,那么你可以将新创建的类继承该类。利用继承的方法,可以重用已存在类的方法和属性,而不用重写这些代码。被继承的类称为超类(super class),派生类称为子类(subclass)。接口在Java中,接口可理解为对象间相互通信的协议。接口在继承中扮演着很重要的角色。接口只定义派生要用到的方法,但是方法的具体实现完全取决于派生类。下一节介绍Java编程中的类和对象。之后你将会对Java中的类和对象有更清楚的认识。————————————————原文链接:https://blog.csdn.net/xqsy2008/article/details/44339091
  • [技术干货] java 基础问题 第二篇
    1.深拷贝和浅拷贝1.1.区别例子:你需要将一个对象拷贝到一个新的对象里(类型相同),你选择浅拷贝,那么它就是将对原先的对象原封不动的拷贝过去(引用类型共享内存地址),而深拷贝就是基本类型复制,引用类型是先创建一个新的对象再将值复制过来区别:引用类型拷贝区别:浅拷贝将地址拷过来(地址复用),深拷贝将值拷过来(创建一个新的对象,地址不复用)浅拷贝定义基本类型字段:直接复制值。引用类型字段:仅复制内存地址(新旧对象共享同一引用对象)。特点:修改原对象或拷贝对象中的引用字段时,另一方会同步变化。深拷贝定义基本类型字段:直接复制值。引用类型字段:递归创建新对象并复制所有层级数据(新旧对象引用独立对象)。特点:修改原对象或拷贝对象中的引用字段时,另一方不受影响。1.2.实现深拷贝的方式实现 Cloneable 接口并重写 clone() 方法class Person implements Cloneable {    String name;    Address address;     @Override    public Person clone() {        try {            Person cloned = (Person) super.clone();            // 深拷贝:递归克隆引用字段            cloned.address = this.address.clone();            return cloned;        } catch (CloneNotSupportedException e) {            throw new AssertionError(); // 不会发生        }    }} class Address implements Cloneable {    String city;     @Override    public Address clone() {        try {            return (Address) super.clone();        } catch (CloneNotSupportedException e) {            throw new AssertionError();        }    }}class Person implements Cloneable {    String name;    Address address;     @Override    public Person clone() {        try {            Person cloned = (Person) super.clone();            // 深拷贝:递归克隆引用字段            cloned.address = this.address.clone();            return cloned;        } catch (CloneNotSupportedException e) {            throw new AssertionError(); // 不会发生        }    }} class Address implements Cloneable {    String city;     @Override    public Address clone() {        try {            return (Address) super.clone();        } catch (CloneNotSupportedException e) {            throw new AssertionError();        }    }}使用序列化和反序列化import java.io.*; class Person implements Serializable {    String name;    Address address; // Address 也需实现 Serializable} public class DeepCopyUtils {    public static <T extends Serializable> T deepCopy(T obj) {        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();             ObjectOutputStream oos = new ObjectOutputStream(bos)) {            oos.writeObject(obj);            try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());                 ObjectInputStream ois = new ObjectInputStream(bis)) {                return (T) ois.readObject();            }        } catch (IOException | ClassNotFoundException e) {            throw new RuntimeException("Deep copy failed", e);        }    }} // 使用示例Person original = new Person();Person deepCopy = DeepCopyUtils.deepCopy(original);手动递归复制class Person {    String name;    Address address;     public Person deepCopy() {        Person copy = new Person();        copy.name = this.name;        copy.address = this.address.deepCopy(); // 手动递归复制        return copy;    }} class Address {    String city;     public Address deepCopy() {        Address copy = new Address();        copy.city = this.city;        return copy;    }} // 使用示例Person original = new Person();Person deepCopy = original.deepCopy();对比总结方法    性能    代码复杂度    适用场景    限制条件Cloneable 接口    高    中等    简单对象、可控的深拷贝    需递归处理引用字段序列化/反序列化    低    低    复杂对象图、完全深拷贝    必须实现 Serializable手动递归复制    高    高    精细控制拷贝逻辑、排除特定字段    代码维护成本高2.泛型2.1.定义泛型是java中的一个特性,它允许在定义类和方法和接口时只需要指定一个或者多个类型参数(泛型符号)就行,在使用时再指定具体的类型2.2.作用当多个类需要共享相同代码逻辑时,可以通过定义泛型来实现。泛型允许在使用时指定具体类型,使不同类都能复用相同的代码逻辑。--------由于泛型在定义时使用类型参数而非具体类型,它可以接收任何类型参数而无需强制类型转换,从而避免了类型安全问题 3.对象3.1.创建对象的方式通过new关键字创建对象通过克隆创建对象通过反射创建对象通过反序列化创建对象3.2.对象回收对象是由垃圾回收器回收,垃圾回收器会在程序运行时自动运行,周期性去检查对象是否被引用,没有就直接回收,释放内存垃圾回收器实现的主要的机制:----1.引用计数法:它会根据该对象的引用计数,如果为0,代表没有被引用直接回收释放---2.可达性分析算法:它会从根对象出发,根据根对象的属性和方法的引用链来判断,如果这个对象没有一个引用链那么代表没有被引用,直接回收释放内存---3.终结器:在对象里面你可以重写finalize()方法,如果重写了该方法,那么垃圾回收器在回收该对象之前会先执行该方法,但是会出现一个问题,你清楚你的这个对象什么时候被回收吗?不确定时间,那么该方法执行的时间也不确定,因此会出现一些可能会出现的问题( 可能导致性能问题、死锁和资源争用),就比如性能问题,如果你在finalize()方法中释放了某些资源,但是由于不确定时间释放,就会导致一些性能问题(可能都不会释放)3.3. 获取私有成员由于定义的私有成员,那么只能该类内部访问,如果外部需要访问你对外提供方法来访问(比getter方法)你通过反射直接获取该成员4.反射4.1.定义反射就是可以获取任何一个类里面的全部信息(父类,实现的接口都可以),并且反射可以调用任何一个类里面的成员4.2.特性运行时类信息全知    只要程序运行,那么通过反射就可以知道类里面全部信息动态的创建对象    由于你知道信息,那么你通过反射也可以创建对象动态的调用方法    也是因为知道全部信息,因此可以调用访问和修改字段值    一样4.3.原理编译器会将源代码先编译成字节码,然后JVM根据字节码翻译成机器码,而反射就是通过字节码从而知道类的全部信息5.异常5.1.异常的种类5.2.处理异常的方法使用try-catch-fianlly(包裹可能会出现异常的代码)手动抛出异常(throw)方法或类抛出异常(throws)如果你使用try-catch-fianlly那么你需要注意:当 try 代码块中的语句发生异常时,后续代码将不会执行,异常会立即传递给 catch 块。如果 catch 块未能捕获对应类型的异常,该异常会继续向上层抛出(例如方法b抛出的异常可被调用它的方法a捕获)。----catch接收到异常那么就会执行catch里面的对应代码----如果定义了fianlly,不管有没有异常,fianlly都会执行(常用于释放锁)----执行顺序规则(fianlly,如果return在前,fianlly在后)return 语句计算返回值:先将返回值存储到临时变量中。执行 finally 块:无论是否有 return 或异常,finally 都会执行。方法返回临时变量中的值:如果 finally 中没有新的 return,则返回最初的值;如果 finally 中有 return,则会覆盖原值。6.Object6.1.等于与equals()区别等于就是:比较基本类型比值,比较引用类型比地址(比表面的)而equals():你如果重写了该方法,那么比较引用类型时比较里面的属性值,没有重写与等于一样6.2.hashcode()与equals()关系前提:你如果要重写这其中的一个方法,那么另外一个也需要重写(约定)一致性:当equals()比较对象相等时,那么hashcode也一定相等(本质就是根据内部值计算出哈希值,而内部值都相等了,你的哈希值肯定相等)不一致性:当hasjcode相等时,equals()不一定相等,因为你不同的对象计算出来的哈希值可能相同(哈希冲突)6.3String、StringBuffer、StringBuilder的区别1.可变性-----解释:在Java中,String对象一旦创建就不可修改,具有不可变性。每次对String进行修改时,实际上都会创建一个新的String对象(数组也有类似特性)。而StringBuffer、StringBuilder可变2.线程安全性---解释:由于String不可变,因此天然的线程安全, 而StringBuilder是线程不安全的(单线程来访问最好),StringBuffer是线程安全的,其内部方法(在StringBuilder方法上)实现了悲观锁synchronized3.性能----解释:由于String每次修改就需要创建一个新的对象,因此性能低, StringBuffer由于内部方法实现了锁(而锁本身就会影响性能),因此性能一般,StringBuilder性能最好 7.序列化7.1.JVM之间的传递对象1.使用序列化和反序列化---解释:你先通过序列化将对象序列化成字节流存入文件中,再将文件传输,接收到文件后再将文件反序列化成对象即可2.使用消息队列---解释:使用消息队列( RabbitMQ、Kafka)来传递消息,另一边接收消息即可 3.使用远程方法(RPC)4.使用数据库(MYSQL)或缓存(Redis)---解释:比如将对象之间存入数据库或缓存中,直接访问数据即可  8.设计模式代理模式:目的:控制对象的访问或增加一些新的方法内容:抽象主题,真实主题,代理三个角色适配器模式:目的:转换接口,是不兼容的类可以并行执行内容:目标接口,适配器,被适配对象9.I/O9.1.实现网络IO高并发编程使用NIO,NIO是同步非阻塞执行的,NIO 是基于I/O多路复用实现的,它可以只用一个线程处理多个客户端I/O,如果你需要同时管理成千上万的连接,但是每个连接只发送少量数据,例如一个聊天服务器,用NIO实现会更好一些。9.2.BIO、NIO、AIO区别BIO    同步阻塞式执行NIO    同步非阻塞执行AIO    异步非阻塞执行解释:同步阻塞式执行:就是说你发完一个消息后,你会一直等待别人回你消息,并且在等待的过程中不会干任何事情同步非阻塞执行:就是说你发完一个消息后,你会一直等待别人回你消息,但是在等待的过程中你可以做一些自己的事情异步非阻塞执行:就是说你发完一个消息后,你不会等待别人回你消息————————————————原文链接:https://blog.csdn.net/2402_88700528/article/details/148211874
  • [技术干货] Java Word转PDF的实现过程
    简介:在IT领域,文档格式转换是常见的任务之一,特别是在管理大量文本数据时。本文将详细探讨如何利用Java技术将Word文档(.docx)转换成PDF格式。转换过程包括文件读取、解析、格式转换等多个技术步骤,并涉及对第三方库的使用。文章假设存在一个名为 DoxcToPdf 的工具或库,用于完成这一转换任务,并对整个过程进行详细解析,包括错误处理和性能优化的考虑。 1. Word到PDF转换概述在当今数字化办公环境中,文档格式的转换是一种常见的需求。将Word文档转换为PDF格式是其中一种重要的转换场景,尤其是在需要保留原文档格式、字体、图片及其他元素以便于分享和打印时。这种转换不仅涉及到文件内容的完整性和一致性,还包括对不同文档结构的理解和处理。在深入探讨技术细节之前,我们先简要了解一下转换流程的宏观概念及其背后的技术原理。1.1 Word到PDF转换的需求背景Microsoft Word格式(.doc或.docx)由于其强大的编辑功能,在创建文档和报告方面广受欢迎。然而,当涉及到跨平台共享、网页发布或打印需求时,PDF格式(便携式文档格式)因其固定的布局和格式而成为更佳的选择。PDF能够确保内容在不同设备和操作系统上的一致性,而且不需要额外的字体或布局软件。1.2 Word到PDF转换的技术路线要实现Word到PDF的转换,可以大致分为以下几个步骤:文件读取 :首先,需要从Word文档中读取内容和格式信息。这通常涉及到文件IO流的操作以及对Word文档结构的理解。内容解析 :解析读取到的内容,将文档中的文本、图片、表格等元素区分开,并提取相关的格式信息。格式转换 :将解析出的内容按照PDF的格式规范重新组织和渲染,生成新的PDF文件。内容重排与样式映射 :为了使PDF文件在视觉上与原Word文档保持一致,可能需要进行内容重排和样式的映射。文件整合与写入 :将转换后的内容整合并写入到PDF文件中。错误处理与性能优化 :确保转换过程的稳定性和性能,处理可能出现的异常情况。在接下来的章节中,我们将详细探讨上述每个步骤的技术细节,包括相关的Java技术栈、库的选择和使用以及最佳实践。通过深入分析这些步骤,你将获得将Word文档转换为PDF的专业技能,并能够优化转换过程以满足不同场景的需求。2. 文件读取技术2.1 Java IO流基础2.1.1 IO流的基本概念在Java中,IO流是进行输入(input)和输出(output)操作的基础。IO流提供了一系列的类和接口,用于处理不同类型的数据传输。在读取文件时,我们通常使用输入流,即从数据源(如文件)读取数据;相反,输出流则是将数据写入目标(如另一个文件)。Java的IO流基于字节流和字符流的概念,字节流主要处理二进制数据,而字符流处理的是字符数据,适用于文本文件。2.1.2 文件读取操作Java提供了 FileInputStream 和 FileReader 等类用于文件的读取。以下是一个简单的文件读取操作的示例代码:import java.io.FileInputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel; public class FileReadExample {    public static void main(String[] args) {        try (FileInputStream fis = new FileInputStream("example.txt")) {            FileChannel fileChannel = fis.getChannel();            ByteBuffer buffer = ByteBuffer.allocate(1024);            int bytesRead = fileChannel.read(buffer);            while (bytesRead != -1) {                buffer.flip();                while (buffer.hasRemaining()) {                    System.out.print((char) buffer.get());                }                buffer.clear();                bytesRead = fileChannel.read(buffer);            }        } catch (IOException e) {            e.printStackTrace();        }    }}在上面的代码中,我们通过 FileInputStream 和 FileChannel 读取了 example.txt 文件,并使用 ByteBuffer 作为数据传输的载体。我们首先读取数据到缓冲区,然后将缓冲区内容打印出来,直到文件末尾。2.2 Java Zip技术解析2.2.1 Zip格式与文件压缩解压原理Zip是一种常用的文件压缩和存档格式,它支持文件的压缩,同时也支持将多个文件存储在单个压缩文件中。Zip格式通过使用压缩算法(如Deflate)来减小文件大小,从而节省存储空间和传输时间。在Java中,我们可以利用Zip相关的类,例如 ZipInputStream 和 ZipOutputStream ,来处理压缩和解压缩任务。2.2.2 使用Zip流读取Word文档在处理Word文档转换为PDF的过程中,可能需要先将压缩包内的.docx文件解压,然后再进行读取。使用 ZipInputStream 可以方便地实现这一过程。下面展示了如何利用 ZipInputStream 来读取Zip文件中的Word文档:import java.io.BufferedInputStream;import java.io.FileInputStream;import java.io.InputStream;import java.util.zip.ZipEntry;import java.util.zip.ZipInputStream; public class ZipReadExample {    public static void main(String[] args) {        try (ZipInputStream zis = new ZipInputStream(new FileInputStream("document.zip"))) {            ZipEntry entry = zis.getNextEntry();            while (entry != null) {                String name = entry.getName();                if (name.endsWith(".docx")) {                    System.out.println("File Found :: " + name);                    // Process the .docx file                    InputStream docxStream = new BufferedInputStream(zis);                    // Read .docx file content here                    docxStream.close();                }                entry = zis.getNextEntry();            }            zis.closeEntry();        } catch (Exception e) {            e.printStackTrace();        }    }}在这个例子中,我们使用 ZipInputStream 来遍历压缩文件中的所有条目,并检查每个条目的名称是否以 .docx 结尾,如果是,则进行后续的处理。这种读取方式为我们处理Word文档提供了便利,尤其是在涉及到复杂文件结构的情况下。在下一章节中,我们会继续深入探讨文件解析技术,其中包含了对XML解析技术的介绍和实战解析示例。3. 文件解析技术文件解析是将文件内容转换成计算机可识别的数据结构的过程,这对于文档处理尤为重要。在进行Word到PDF转换的过程中,我们需要深入理解文件格式,并据此解析、提取并重组内容。接下来,我们将对解析技术进行详细介绍,尤其是XML解析技术,这在处理如.docx这样的基于XML的现代文档格式中是不可或缺的。3.1 XML解析技术简介3.1.1 XML的组成与结构XML(可扩展标记语言)是一种标记语言,设计用来存储和传输数据。与HTML不同,XML不是为了显示数据而设计的,而是专注于数据内容的描述。它是一种元语言,用于定义其他特定领域的标记语言,从而允许用户定义自己的标签和属性。一个基本的XML文档由元素组成,这些元素以标签的形式出现。每个标签可以包含属性,还可以嵌套其他标签。XML文档必须有且仅有一个根元素,这表示文档的开始和结束。此外,XML还严格要求标签正确嵌套,所有标签都必须被关闭。下面是一个简单的XML文档示例:<?xml version="1.0" encoding="UTF-8"?><bookstore>    <book category="cooking">        <title lang="en">Everyday Italian</title>        <author>Giada De Laurentiis</author>        <year>2005</year>        <price>30.00</price>    </book></bookstore>3.1.2 解析XML的优势解析XML的优势在于其可读性强,以及便于进行数据交换。由于XML文档具有自我描述性质,因此更容易被不同的应用程序理解,这一点对于文档格式转换尤为重要。在转换过程中,XML允许我们精确地定位并处理文档中的各个部分,无论其结构多么复杂。另一个显著的优势是,XML文档的解析可以通过多种方法实现,这为开发人员提供了灵活性。例如,可以使用DOM解析器将整个文档加载到内存中作为树状结构进行操作;也可以使用SAX解析器逐个处理XML中的事件,这种方式对内存的需求较小。3.2 Java XML解析器实战3.2.1 JAXB解析示例JAXB(Java Architecture for XML Binding)是一个强大的库,可以将Java对象序列化为XML格式,或者将XML文档反序列化为Java对象。通过JAXB,我们可以更轻松地处理XML,因为我们可以操作对象而不是直接处理文本。下面的代码示例展示了如何使用JAXB将Java对象序列化为XML文件:import javax.xml.bind.annotation.XmlRootElement;import javax.xml.bind.annotation.XmlElement;import javax.xml.bind.JAXBContext;import javax.xml.bind.JAXBException;import javax.xml.bind.Marshaller; @XmlRootElementclass Book {    private String title;    private String author;    private int year;    private double price;    // Getters and setters...} public class JAXBExample {    public static void main(String[] args) {        try {            Book book = new Book();            book.setTitle("Everyday Italian");            book.setAuthor("Giada De Laurentiis");            book.setYear(2005);            book.setPrice(30.00);             JAXBContext context = JAXBContext.newInstance(Book.class);            Marshaller marshaller = context.createMarshaller();            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);            marshaller.marshal(book, System.out);        } catch (JAXBException e) {            e.printStackTrace();        }    }}3.2.2 DOM4J解析示例DOM4J是一个开源的Java XML API,用于读写XML文档。它支持DOM、SAX和JAXP,但主要侧重于SAX的性能和灵活性。DOM4J使用XPath表达式和XSLT转换作为核心API的一部分。下面的代码示例展示了如何使用DOM4J来解析一个简单的XML文档,并打印出根元素和其子元素:import org.dom4j.Document;import org.dom4j.DocumentException;import org.dom4j.DocumentHelper;import org.dom4j.Element; public class DOM4JExample {    public static void main(String[] args) {        try {            String xmlContent = "<bookstore><book><title>Everyday Italian</title><author>Giada De Laurentiis</author></book></bookstore>";            Document document = DocumentHelper.parseText(xmlContent);            Element rootElement = document.getRootElement();            System.out.println("Root element: " + rootElement.getName());            for (Element element : (List<Element>) rootElement.elements()) {                System.out.println("Child element: " + element.getName() + ", text: " + element.getText());            }        } catch (DocumentException e) {            e.printStackTrace();        }    }}通过使用这些示例,我们可以看到XML解析技术的多样性和实用性。这些技术的应用能够确保在进行Word到PDF转换时,能够准确无误地处理文档结构,确保最终输出的PDF文件内容准确且格式整洁。4. 格式转换技术4.1 处理Word文档4.1.1 Apache POI 库基础Apache POI是Apache Software Foundation的一个Java库,它提供了一套用于读写Microsoft Office格式文件的API。使用Apache POI可以轻松地处理Word文档,如.doc和.docx文件格式。 HSSF (Horrible Spreadsheet Format)和 XSSF 是Apache POI的两个子项目,分别用于处理旧版的 .xls 和较新的 .xlsx 格式的Excel文档。在处理Word文档方面, HWPF (Horrible Word Processor Format)用于处理 .doc 格式的文档,而 XWPF (XML Word Processor Format)则用于处理 .docx 格式的文档。要使用Apache POI库,首先需要将其添加到项目的依赖中。对于Maven项目,可以在 pom.xml 文件中添加以下依赖:<dependency>    <groupId>org.apache.poi</groupId>    <artifactId>poi</artifactId>    <version>版本号</version></dependency><dependency>    <groupId>org.apache.poi</groupId>    <artifactId>poi-ooxml</artifactId>    <version>版本号</version></dependency>Apache POI的设计是高度面向对象的,通过对象模型来模拟Word文档的结构。例如,在处理 .docx 格式时, XWPFDocument 类代表了一个Word文档对象,而 XWPFParagraph 代表一个段落, XWPFRun 代表段落内的文本运行(文本格式化)等。4.1.2 读取.docx文档内容读取 .docx 文档内容需要创建 XWPFDocument 对象,并通过该对象的API来访问文档的不同部分。下面的代码示例展示了如何读取 .docx 文档中的所有文本内容:import org.apache.poi.xwpf.usermodel.XWPFDocument;import org.apache.poi.xwpf.usermodel.XWPFParagraph; import java.io.FileInputStream;import java.io.IOException; public class ReadDocxExample {    public static void main(String[] args) {        try (FileInputStream fis = new FileInputStream("example.docx")) {            XWPFDocument document = new XWPFDocument(fis);            for (XWPFParagraph para : document.getParagraphs()) {                System.out.println(para.getText());            }        } catch (IOException e) {            e.printStackTrace();        }    }}在上述代码中,我们首先使用 FileInputStream 打开一个 .docx 文件,然后创建一个 XWPFDocument 对象来代表这个Word文档。通过调用 document.getParagraphs() 方法,我们可以获取文档中的所有段落,并遍历输出每个段落的内容。4.2 生成PDF文件4.2.1 iText 库使用方法iText 是一个强大的Java库,可以用来创建和操纵PDF文档。它提供了一系列API来生成PDF文件,包括文字、图像、表格、表单等。 iText 还支持将现有的Word文档转换成PDF格式。从2019年开始,iText以商业开源许可证发布,因此在使用之前需要确保许可证的合规性。要使用 iText ,你需要将以下依赖添加到项目的 pom.xml 文件中:<dependency>    <groupId>com.itextpdf</groupId>    <artifactId>itext7-core</artifactId>    <version>版本号</version></dependency>以下是使用 iText 7 将文本写入PDF文件的一个简单示例:import com.itextpdf.kernel.pdf.PdfDocument;import com.itextpdf.kernel.pdf.PdfWriter;import com.itextpdf.layout.Document;import com.itextpdf.layout.element.Paragraph; import java.io.FileNotFoundException; public class CreatePdfExample {    public static void main(String[] args) {        String dest = "output.pdf";        try (PdfWriter writer = new PdfWriter(dest);             PdfDocument pdf = new PdfDocument(writer);             Document document = new Document(pdf)) {            document.add(new Paragraph("Hello, World!"));        } catch (FileNotFoundException e) {            e.printStackTrace();        }    }}在这段代码中,我们首先创建了 PdfWriter 和 PdfDocument 对象。 PdfWriter 负责写入PDF文件, PdfDocument 代表整个PDF文档。然后,我们创建了一个 Document 对象,该对象代表正在操作的PDF文件。最后,我们向 Document 对象添加了一个包含文本的 Paragraph 元素,然后将PDF写入到指定的文件。4.2.2 PDFBox 库使用方法Apache PDFBox 是Apache软件基金会提供的一个开源项目,它是一个用来创建和操作PDF文档的Java库。这个库的功能包括创建新的PDF文档、渲染PDF内容以及从PDF文档中提取文字和图像等。与 iText 不同, PDFBox 更侧重于对PDF文件的读取和修改,而不是创建复杂的排版。首先,你需要将 PDFBox 添加到你的项目依赖中:<dependency>    <groupId>org.apache.pdfbox</groupId>    <artifactId>pdfbox</artifactId>    <version>版本号</version></dependency>下面的代码示例演示了如何使用 PDFBox 读取PDF文件并输出文件中的所有文本内容:import org.apache.pdfbox.pdmodel.PDDocument;import org.apache.pdfbox.text.PDFTextStripper; import java.io.File;import java.io.IOException; public class ReadPdfExample {    public static void main(String[] args) {        try (PDDocument document = PDDocument.load(new File("input.pdf"))) {            PDFTextStripper stripper = new PDFTextStripper();            String pdfContent = stripper.getText(document);            System.out.println(pdfContent);        } catch (IOException e) {            e.printStackTrace();        }    }}在这个例子中,我们使用 PDFTextStripper 类来获取PDF文档中的文本。首先加载一个PDF文件到 PDDocument 对象中,然后创建 PDFTextStripper 实例。调用 stripper.getText(document) 方法后,文档的全部文本内容将被提取并存储在字符串变量 pdfContent 中,然后输出。这两个工具— iText 和 PDFBox —提供了不同的功能,开发者可以根据具体需求选择合适的库来实现Word到PDF的转换。5. 内容重排与样式映射在将Word文档转换为PDF的过程中,内容重排与样式映射是确保转换质量的关键环节。合理的内容重排策略能够提升文档的可读性,而精确的样式映射则是保证最终PDF文件视觉效果一致性的基础。5.1 文档内容的重排策略内容重排主要关注文档的逻辑结构,合理的重排能够使信息传达更为清晰。5.1.1 理解文档结构的重要性在转换过程中,首先需要识别文档中的标题、段落、列表等元素。这是因为不同元素可能需要不同的布局和格式处理。例如,标题可能需要较大的字体和加粗样式,而列表项则可能需要缩进和特定的项目符号。// 示例代码:使用Apache POI解析.docx文档中的结构XWPFDocument document = new XWPFDocument(OPCPackage.open(new File("example.docx").getAbsolutePath()));List<XWPFParagraph> paragraphs = document.getParagraphs();for (XWPFParagraph paragraph : paragraphs) {    CTParagraph ctParagraph = paragraph.getCTP();    List<CTR> elements = ctParagraph.getAbstractNumList();    // 遍历段落中的元素以识别结构    for (CTR element : elements) {        // ...    }}5.1.2 实现内容的逻辑重排逻辑重排通常涉及创建新的段落和列表,保持原有的文档格式但优化其展示。例如,在PDF中,原有的Word文档的标题可以通过分页和增加标题样式来获得更好的视觉效果。// 示例代码:在iText中创建新的段落和列表PdfPTable table = new PdfPTable(1);PdfPCell cell = new PdfPCell();cell.addElement(new Paragraph("Title 1"));cell.addElement(new Paragraph("Item 1"));table.addCell(cell);// 继续添加其他项...5.2 样式映射机制样式映射关注的是将Word文档中的样式转换为PDF文档中的等效样式。5.2.1 Word样式与PDF样式的对应关系Word文档中包含多种内建和自定义的样式,这些样式在PDF中可能没有直接的等效样式。因此,需要定义一个映射表来规定样式转换规则,比如将Word中的“标题1”样式映射为PDF中的“Heading 1”样式。// 示例代码:使用iText进行样式映射Map<String, String> styleMapping = new HashMap<>();styleMapping.put("Heading 1", PdfName.HEADING_1.toString());styleMapping.put("Heading 2", PdfName.HEADING_2.toString());// 其他样式映射...5.2.2 样式转换的实际应用案例在实际应用中,可能需要处理的样式类型不仅限于标题和列表,还可能包括图片、表格和引用等。样式转换的代码会根据不同文档的需要进行相应的调整。// 示例代码:转换段落样式Paragraph paragraph = new Paragraph();for (XWPFParagraph p : paragraphs) {    String style = p.getParagraphFormat().getBuiltInStyleId();    if (styleMapping.containsKey(style)) {        paragraph.add(new Paragraph(p.getText(), styleMapping.get(style)));    } else {        paragraph.add(new Paragraph(p.getText())); // 默认样式    }}在这一章节中,我们讨论了内容重排与样式映射的重要性,并通过代码示例展示了如何在实际应用中进行操作。下一章节将继续探讨如何整合所有模块,并实现最终的PDF文件写入。本文还有配套的精品资源,点击获取 简介:在IT领域,文档格式转换是常见的任务之一,特别是在管理大量文本数据时。本文将详细探讨如何利用Java技术将Word文档(.docx)转换成PDF格式。转换过程包括文件读取、解析、格式转换等多个技术步骤,并涉及对第三方库的使用。文章假设存在一个名为 DoxcToPdf 的工具或库,用于完成这一转换任务,并对整个过程进行详细解析,包括错误处理和性能优化的考虑。————————————————原文链接:https://blog.csdn.net/weixin_42181686/article/details/146289533
  • [技术干货] Java内存区域与内存溢出异常分析与解决【转】
    一、Java 内存区域概述1.1 程序计数器(Program Counter Register)程序计数器是当前线程所执行的字节码的行号指示器。它不会出现内存溢出问题。1.2 Java 虚拟机栈(Java Virtual Machine Stacks)每个线程都有一个私有的虚拟机栈,用于存储方法调用过程中的局部变量、操作数栈等信息。栈溢出场景12345678910111213141516171819public class JavaVMStackSOF {    private int stackLength = 1;     public void stackLeak() {        stackLength++;        stackLeak();    }     public static void main(String[] args) throws Throwable {        JavaVMStackSOF oom = new JavaVMStackSOF();         try {            oom.stackLeak();        } catch (Throwable e) {            System.out.println("stack length: " + oom.stackLength);            throw e;        }    }}运行结果:stack length: 2402Exception in thread "main" java.lang.StackOverflowError1.3 本地方法栈(Native Method Stacks)本地方法栈与虚拟机栈类似,但用于存储本地方法(Native 方法)的调用信息。1.4 Java 堆(Java Heap)Java 堆是所有线程共享的内存区域,用于存储对象实例和数组。堆溢出场景123456789101112import java.util.ArrayList; public class HeapOOM {    static class OOMObject {}     public static void main(String[] args) {        ArrayList<OOMObject> list = new ArrayList<>();        while (true) {            list.add(new OOMObject());        }    }}运行结果:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space1.5 方法区(Method Area)方法区用于存储类的结构信息、常量池、方法数据等。方法区溢出场景1234567891011121314151617181920import java.util.ArrayList;import java.util.List; public class MethodAreaOOM {    public static void main(String[] args) {        List<Class<?>> list = new ArrayList<>();        int i = 0;        while (true) {            list.add(new MyClassLoader().findClass("com.example.DummyClass" + i++));        }    } ​​​​​​​    static class MyClassLoader extends ClassLoader {        @Override        protected Class<?> findClass(String name) {            byte[] b = new byte[0];            return defineClass(name, b, 0, b.length);        }    }}运行结果:Exception in thread "main" java.lang.OutOfMemoryError: Metaspace1.6 本机直接内存(Direct Memory)本机直接内存用于直接内存操作,通常通过 ByteBuffer 使用。直接内存溢出场景123456789101112131415import java.nio.ByteBuffer; public class DirectMemoryOOM {    private static final int _1MB = 1024 * 1024;     public static void main(String[] args) {        try {            while (true) {                ByteBuffer.allocateDirect(_1MB);            }        } catch (Throwable e) {            e.printStackTrace();        }    }}运行结果:Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory二、内存溢出异常及其解决方法2.1 Java 堆溢出原因Java 堆用于存储对象实例,当不断创建对象且 GC Roots 到对象之间有可达路径时,堆内存耗尽会引发 OutOfMemoryError。解决方法使用内存映像分析工具(如 Eclipse Memory Analyzer)分析堆转储快照,确定是内存泄漏还是内存溢出。调整虚拟机堆参数(-Xmx 和 -Xms)。优化代码,减少不必要的对象引用,缩短对象生命周期。2.2 虚拟机栈和本地方法栈溢出原因线程请求的栈深度超过虚拟机允许的最大深度会引发 StackOverflowError;如果动态扩展栈时无法申请到足够内存,则引发 OutOfMemoryError。解决方法调整栈大小参数(-Xss)。优化递归算法,减少栈深度。2.3 方法区溢出原因方法区存储类的结构信息,动态生成大量类(如使用 CGLib)会导致方法区溢出。解决方法调整方法区大小参数(-XX:PermSize 和 -XX:MaxPermSize)。优化类的加载和卸载机制。2.4 运行时常量池溢出原因运行时常量池存储字符串常量等数据,当常量池满且无法扩展时会引发溢出。解决方法调整方法区大小参数。避免大量动态生成字符串常量。2.5 本机直接内存溢出原因直接内存用于直接内存操作,当直接内存耗尽时会引发溢出。解决方法调整直接内存大小参数(-XX:MaxDirectMemorySize)。优化代码,及时释放直接内存。
总条数:691 到第
上滑加载中