目前数据治理服务中有众多治理任务,当其中任一治理任务有改动需要升级或新增一个治理任务时,都需要将数据治理服务重启,会影响其他治理任务的正常运行。
URLClassLoader 是一种特殊的类加载器,可以从指定的 URL 中加载类和资源。它的主要作用是动态加载外部的 JAR 包或者类文件,从而实现动态扩展应用程序的功。为了便于管理动态加载的jar包,自定义类加载器继承URLClassloader。
package cn.jy.sjzl.util;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 自定义类加载器
*
* @author lijianyu
* @date 2023/04/03 17:54
**/
public class MyClassLoader extends URLClassLoader {
private Map<String, Class<?>> loadedClasses = new ConcurrentHashMap<>();
public Map<String, Class<?>> getLoadedClasses() {
return loadedClasses;
}
public MyClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 从已加载的类集合中获取指定名称的类
Class<?> clazz = loadedClasses.get(name);
if (clazz != null) {
return clazz;
}
try {
// 调用父类的findClass方法加载指定名称的类
clazz = super.findClass(name);
// 将加载的类添加到已加载的类集合中
loadedClasses.put(name, clazz);
return clazz;
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
public void unload() {
try {
for (Map.Entry<String, Class<?>> entry : loadedClasses.entrySet()) {
// 从已加载的类集合中移除该类
String className = entry.getKey();
loadedClasses.remove(className);
try{
// 调用该类的destory方法,回收资源
Class<?> clazz = entry.getValue();
Method destory = clazz.getDeclaredMethod("destory");
destory.invoke(clazz);
} catch (Exception e ) {
// 表明该类没有destory方法
}
}
// 从其父类加载器的加载器层次结构中移除该类加载器
close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
由于此项目使用spring框架,以及xxl-job任务的机制调用动态加载的代码,因此要完成以下内容
package com.jy.dynamicLoad;
import com.jy.annotation.XxlJobCron;
import com.jy.classLoader.MyClassLoader;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import com.xxl.job.core.handler.annotation.XxlJob;
import com.xxl.job.core.handler.impl.MethodJobHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* @author lijianyu
* @date 2023/04/29 13:18
**/
@Component
public class DynamicLoad {
private static Logger logger = LoggerFactory.getLogger(DynamicLoad.class);
@Autowired
private ApplicationContext applicationContext;
private Map<String, MyClassLoader> myClassLoaderCenter = new ConcurrentHashMap<>();
@Value("${dynamicLoad.path}")
private String path;
/**
* 动态加载指定路径下指定jar包
* @param path
* @param fileName
* @param isRegistXxlJob 是否需要注册xxljob执行器,项目首次启动不需要注册执行器
* @return map<jobHander, Cron> 创建xxljob任务时需要的参数配置
*/
public void loadJar(String path, String fileName, Boolean isRegistXxlJob) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
File file = new File(path +"/" + fileName);
Map<String, String> jobPar = new HashMap<>();
// 获取beanFactory
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
// 获取当前项目的执行器
try {
// URLClassloader加载jar包规范必须这么写
URL url = new URL("jar:file:" + file.getAbsolutePath() + "!/");
URLConnection urlConnection = url.openConnection();
JarURLConnection jarURLConnection = (JarURLConnection)urlConnection;
// 获取jar文件
JarFile jarFile = jarURLConnection.getJarFile();
Enumeration<JarEntry> entries = jarFile.entries();
// 创建自定义类加载器,并加到map中方便管理
MyClassLoader myClassloader = new MyClassLoader(new URL[] { url }, ClassLoader.getSystemClassLoader());
myClassLoaderCenter.put(fileName, myClassloader);
Set<Class> initBeanClass = new HashSet<>(jarFile.size());
// 遍历文件
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
if (jarEntry.getName().endsWith(".class")) {
// 1. 加载类到jvm中
// 获取类的全路径名
String className = jarEntry.getName().replace('/', '.').substring(0, jarEntry.getName().length() - 6);
// 1.1进行反射获取
myClassloader.loadClass(className);
}
}
Map<String, Class<?>> loadedClasses = myClassloader.getLoadedClasses();
XxlJobSpringExecutor xxlJobExecutor = new XxlJobSpringExecutor();
for(Map.Entry<String, Class<?>> entry : loadedClasses.entrySet()){
String className = entry.getKey();
Class<?> clazz = entry.getValue();
// 2. 将有@spring注解的类交给spring管理
// 2.1 判断是否注入spring
Boolean flag = SpringAnnotationUtils.hasSpringAnnotation(clazz);
if(flag){
// 2.2交给spring管理
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
// 此处beanName使用全路径名是为了防止beanName重复
String packageName = className.substring(0, className.lastIndexOf(".") + 1);
String beanName = className.substring(className.lastIndexOf(".") + 1);
beanName = packageName + beanName.substring(0, 1).toLowerCase() + beanName.substring(1);
// 2.3注册到spring的beanFactory中
beanFactory.registerBeanDefinition(beanName, beanDefinition);
// 2.4允许注入和反向注入
beanFactory.autowireBean(clazz);
beanFactory.initializeBean(clazz, beanName);
/*if(Arrays.stream(clazz.getInterfaces()).collect(Collectors.toSet()).contains(InitializingBean.class)){
initBeanClass.add(clazz);
}*/
initBeanClass.add(clazz);
}
// 3. 带有XxlJob注解的方法注册任务
// 3.1 过滤方法
Map<Method, XxlJob> annotatedMethods = null;
try {
annotatedMethods = MethodIntrospector.selectMethods(clazz,
new MethodIntrospector.MetadataLookup<XxlJob>() {
@Override
public XxlJob inspect(Method method) {
return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
}
});
} catch (Throwable ex) {
}
// 3.2 生成并注册方法的JobHander
for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
Method executeMethod = methodXxlJobEntry.getKey();
// 获取jobHander和Cron
XxlJobCron xxlJobCron = executeMethod.getAnnotation(XxlJobCron.class);
if(xxlJobCron == null){
throw new CustomException("500", executeMethod.getName() + "(),没有添加@XxlJobCron注解配置定时策略");
}
if (!CronExpression.isValidExpression(xxlJobCron.value())) {
throw new CustomException("500", executeMethod.getName() + "(),@XxlJobCron参数内容错误");
}
XxlJob xxlJob = methodXxlJobEntry.getValue();
jobPar.put(xxlJob.value(), xxlJobCron.value());
if (isRegistXxlJob) {
executeMethod.setAccessible(true);
// regist
Method initMethod = null;
Method destroyMethod = null;
xxlJobExecutor.registJobHandler(xxlJob.value(), new CustomerMethodJobHandler(clazz, executeMethod, initMethod, destroyMethod));
}
}
}
// spring bean实际注册
initBeanClass.forEach(beanFactory::getBean);
} catch (IOException e) {
logger.error("读取{} 文件异常", fileName);
e.printStackTrace();
throw new RuntimeException("读取jar文件异常: " + fileName);
}
}
}
以下是判断该类是否有spring注解的工具类
public class SpringAnnotationUtils {
private static Logger logger = LoggerFactory.getLogger(SpringAnnotationUtils.class);
/**
* 判断一个类是否有 Spring 核心注解
*
* @param clazz 要检查的类
* @return true 如果该类上添加了相应的 Spring 注解;否则返回 false
*/
public static boolean hasSpringAnnotation(Class<?> clazz) {
if (clazz == null) {
return false;
}
//是否是接口
if (clazz.isInterface()) {
return false;
}
//是否是抽象类
if (Modifier.isAbstract(clazz.getModifiers())) {
return false;
}
try {
if (clazz.getAnnotation(Component.class) != null ||
clazz.getAnnotation(Repository.class) != null ||
clazz.getAnnotation(Service.class) != null ||
clazz.getAnnotation(Controller.class) != null ||
clazz.getAnnotation(Configuration.class) != null) {
return true;
}
}catch (Exception e){
logger.error("出现异常:{}",e.getMessage());
}
return false;
}
}
注册xxljob执行器的操作是仿照的xxljob中的XxlJobSpringExecutor的注册方法。
动态卸载的过程,就是将动态加载的代码,从内存,spring以及xxljob中移除。
代码如下:
/**
* 动态卸载指定路径下指定jar包
* @param fileName
* @return map<jobHander, Cron> 创建xxljob任务时需要的参数配置
*/
public void unloadJar(String fileName) throws IllegalAccessException, NoSuchFieldException {
// 获取加载当前jar的类加载器
MyClassLoader myClassLoader = myClassLoaderCenter.get(fileName);
// 获取jobHandlerRepository私有属性,为了卸载xxljob任务
Field privateField = XxlJobExecutor.class.getDeclaredField("jobHandlerRepository");
// 设置私有属性可访问
privateField.setAccessible(true);
// 获取私有属性的值jobHandlerRepository
XxlJobExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
Map<String, IJobHandler> jobHandlerRepository = (ConcurrentHashMap<String, IJobHandler>) privateField.get(xxlJobSpringExecutor);
// 获取beanFactory,准备从spring中卸载
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
Map<String, Class<?>> loadedClasses = myClassLoader.getLoadedClasses();
Set<String> beanNames = new HashSet<>();
for (Map.Entry<String, Class<?>> entry: loadedClasses.entrySet()) {
// 1. 将xxljob任务从xxljob执行器中移除
// 1.1 截取beanName
String key = entry.getKey();
String packageName = key.substring(0, key.lastIndexOf(".") + 1);
String beanName = key.substring(key.lastIndexOf(".") + 1);
beanName = packageName + beanName.substring(0, 1).toLowerCase() + beanName.substring(1);
// 获取bean,如果获取失败,表名这个类没有加到spring容器中,则跳出本次循环
Object bean = null;
try{
bean = applicationContext.getBean(beanName);
}catch (Exception e){
// 异常说明spring中没有这个bean
continue;
}
// 1.2 过滤方法
Map<Method, XxlJob> annotatedMethods = null;
try {
annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
new MethodIntrospector.MetadataLookup<XxlJob>() {
@Override
public XxlJob inspect(Method method) {
return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
}
});
} catch (Throwable ex) {
}
// 1.3 将job从执行器中移除
for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
XxlJob xxlJob = methodXxlJobEntry.getValue();
jobHandlerRepository.remove(xxlJob.value());
}
// 2.0从spring中移除,这里的移除是仅仅移除的bean,并未移除bean定义
beanNames.add(beanName);
beanFactory.destroyBean(beanName, bean);
}
// 移除bean定义
Field mergedBeanDefinitions = beanFactory.getClass()
.getSuperclass()
.getSuperclass().getDeclaredField("mergedBeanDefinitions");
mergedBeanDefinitions.setAccessible(true);
Map<String, RootBeanDefinition> rootBeanDefinitionMap = ((Map<String, RootBeanDefinition>) mergedBeanDefinitions.get(beanFactory));
for (String beanName : beanNames) {
beanFactory.removeBeanDefinition(beanName);
// 父类bean定义去除
rootBeanDefinitionMap.remove(beanName);
}
// 卸载父任务,子任务已经在循环中卸载
jobHandlerRepository.remove(fileName);
// 3.2 从类加载中移除
try {
// 从类加载器底层的classes中移除连接
Field field = ClassLoader.class.getDeclaredField("classes");
field.setAccessible(true);
Vector<Class<?>> classes = (Vector<Class<?>>) field.get(myClassLoader);
classes.removeAllElements();
// 移除类加载器的引用
myClassLoaderCenter.remove(fileName);
// 卸载类加载器
myClassLoader.unload();
} catch (NoSuchFieldException e) {
logger.error("动态卸载的类,从类加载器中卸载失败");
e.printStackTrace();
} catch (IllegalAccessException e) {
logger.error("动态卸载的类,从类加载器中卸载失败");
e.printStackTrace();
}
logger.error("{} 动态卸载成功", fileName);
}
使用动态加载时,为了避免服务重新启动后丢失已加载的任务包,使用动态配置的方式,加载后动态更新初始化加载配置。
以下提供了两种自己实际操作过的配置方式。
动态修改本地yml配置文件,需要添加snakeyaml的依赖
4.1.1 依赖引入
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.29</version>
</dependency>
4.1.2 工具类
读取指定路径下的配置文件,并进行修改。
package com.jy.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 用于动态修改bootstrap.yml配置文件
* @author lijianyu
* @date 2023/04/18 17:57
**/
@Component
public class ConfigUpdater {
public void updateLoadJars(List<String> jarNames) throws IOException {
// 读取bootstrap.yml
Yaml yaml = new Yaml();
InputStream inputStream = new FileInputStream(new File("src/main/resources/bootstrap.yml"));
Map<String, Object> obj = yaml.load(inputStream);
inputStream.close();
obj.put("loadjars", jarNames);
// 修改
FileWriter writer = new FileWriter(new File("src/main/resources/bootstrap.yml"));
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setPrettyFlow(true);
Yaml yamlWriter = new Yaml(options);
yamlWriter.dump(obj, writer);
}
}
Spring Cloud Alibaba Nacos组件完全支持在运行时通过代码动态修改配置,还提供了一些API供开发者在代码里面实现动态修改配置。在每次动态加载或卸载数据治理任务jar包时,执行成功后都会进行动态更新nacos配置。
package cn.jy.sjzl.config;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.exception.NacosException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
@Configuration
public class NacosConfig {
@Value("${spring.cloud.nacos.server-addr}")
private String serverAddr;
@Value("${spring.cloud.nacos.config.namespace}")
private String namespace;
public ConfigService configService() throws NacosException {
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
properties.put("namespace", namespace);
return NacosFactory.createConfigService(properties);
}
}
package cn.jy.sjzl.util;
import cn.jy.sjzl.config.NacosConfig;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.api.config.ConfigService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* nacos配置中,修改sjzl-loadjars.yml
*
* @author lijianyu
* @date 2023/04/19 17:59
**/
@Component
public class NacosConfigUtil {
private static Logger logger = LoggerFactory.getLogger(NacosConfigUtil.class);
@Autowired
private NacosConfig nacosConfig;
private String dataId = "sjzl-loadjars.yml";
@Value("${spring.cloud.nacos.config.group}")
private String group;
/**
* 从nacos配置文件中,添加初始化jar包配置
* @param jarName 要移除的jar包名
* @throws Exception
*/
public void addJarName(String jarName) throws Exception {
ConfigService configService = nacosConfig.configService();
String content = configService.getConfig(dataId, group, 5000);
// 修改配置文件内容
YAMLMapper yamlMapper = new YAMLMapper();
ObjectMapper jsonMapper = new ObjectMapper();
Object yamlObject = yamlMapper.readValue(content, Object.class);
String jsonString = jsonMapper.writeValueAsString(yamlObject);
JSONObject jsonObject = JSONObject.parseObject(jsonString);
List<String> loadjars;
if (jsonObject.containsKey("loadjars")) {
loadjars = (List<String>) jsonObject.get("loadjars");
}else{
loadjars = new ArrayList<>();
}
if (!loadjars.contains(jarName)) {
loadjars.add(jarName);
}
jsonObject.put("loadjars" , loadjars);
Object yaml = yamlMapper.readValue(jsonMapper.writeValueAsString(jsonObject), Object.class);
String newYamlString = yamlMapper.writeValueAsString(yaml);
boolean b = configService.publishConfig(dataId, group, newYamlString);
if(b){
logger.info("nacos配置更新成功");
}else{
logger.info("nacos配置更新失败");
}
}
}
分离打包时,根据实际情况在pom.xml中修改以下配置
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<includes>
<include>com/jy/job/demo/**</include>
</includes>
</filter>
</filters>
<finalName>demoJob</finalName>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
本文由哈喽比特于9月以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/rj3l-w_G2ig1vopZ1M4tRQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。