Web后端开发

HTTP协议

Hyper Text Transfer Protocol

  • 基于TCP
  • 基于请求-响应模型
  • 无状态的协议,不带之前的内容

会话

请求协议

请求行 请求方式,资源路径,协议

请求头

请求体 和请求头有一个空行

响应协议

响应行 协议,状态码,描述

  • 1xx 响应中
  • 2xx 成功
  • 3xx 重定向
  • 4xx 客户端错误
  • 5xx 服务器错误

响应头

响应体

协议解析

Apache Tomcat

webapps目录

conf/server.xml 改端口

默认端口80

logging.properties 改UTF-8

DispatcherServlet 前端控制器

HttpServletRequest 请求对象

HttpServletResponse 响应对象

请求响应

请求

  • 简单参数
  • 实体参数
  • 数组 String[]/@RequstParam List
    • @RequestParam(defaultValue=“1”)如果没给参数默认值
  • 日期 DateTimeFormat
  • Json @RequestBody
    • @RequestBody Dept dept
  • 路径 {id} @PathVariable
    • @PathVariable Integer id

响应

@ResponseBody

@RestController=@ResponseBody+@Controller

统一响应结果

public class Result{
    private Integer code;
    private String msg;
    private Object data;
    ……
}

解析xml文件 dom4j

@RequestMapping(value="/depts"method=RequestMethod.GET)
->
@GetMapping("")
    ...

分层解耦

Controller

Service

Dao

  • 内聚
  • 耦合

解耦 容器

  • 控制反转 impl->容器
    • Inversion Of Control
  • 依赖注入 容器->service
    • Dependency Injection
  • Bean IOC容器中创建、管理的对象

@Component 控制反转,交给容器

@Autowired 依赖注入

IOC

  • @Component
  • @Controller
  • @Service
  • @Repository

组件扫描

@ComponentScan({“…”,“…”})

指定扫描范围,默认本包和子包

包含在@SpringBootApplication

DI

Bean注入报错

  • @Primary
  • @Qualifer
  • @Resource

MySQL

DataBase Management System

Structured Query Language SQL

  • DDL 数据定义语言
  • DML 数据操作语言
  • DQL 数据查询语言
  • DCL 数据控制语言

DDL

show databases;
select database();

create database ;
drop database ;

use ;

--也可以用schema
create table 表名(
    字段 字段类型 [约束] [comment 字段注释]
)[comment 表注释]
  • not null
  • unique
  • primary key
  • default
  • foreign key

数据类型

  • 数值类型
  • 字符串类型
  • 日期时间类型
show tables;
desc 表名;
show create table 表名;
alter table 表名 add 字段名 类型 [comment 注释][约束];
alter table 表名 modify 字段名 数据类型;
alter table 表名 change 旧字段名 新字段名 类型 [comment 注释][约束];
alter table 表名 drop column 字段名;
rename table 表名 to 新表名;
drop table if exists 表名;

DML

insert

update

delete

DQL

SELECT

基本查询

select 
from
where
group by
having
order by
limit

条件查询

>=<
between ... and ...
in ...
like _ %
(查询两个字 like '__'is null
and &&
or ||
not !

分组查询

-- 聚合函数
count
-- count (字段)
-- count (常量)
-- count (*)
max
min
avg
sum

group by 字段

having 分组后的条件(where在分组之前过滤)

  • 分组之后,查询的一般为聚合函数和分组字段
  • where->聚合函数->having

排序查询

order by

ASC 升序 DESC 降序

多个排序字段用 ,

分页查询

limit 起始索引,查询记录数

第一页的索引是0,相应的,第二页索引是查询记录数

如果查询第一页可以不写起始索引

案例

select if(gender = 1,'男性员工','女性员工') 性别,count(*) from emp group by gende;
-- true第二个值,false第三个值,后面别名是筛选出的列名
-- 就是把拿到的gender处理

select
(case job when 1 then '班主任' ... else '未分配' end) 职位
form emp group by job;

多表设计

一对多

外键约束 一致性完整性

[constraint] [外键名称] foreign key (外键字段名) references 主表(字段名)

alter table 表名 add constraint 外键名称 foreign key (外键字段名) references 主表(字段名)

-- 外键约束添加在子表,也就是一对多的多
  • 物理外键
    • 影响增删改效率
    • 仅用于单节点数据库不适用分布式、集群
    • 容易引发死锁
  • 逻辑外键

一对一

任意一方加入外键,关联另外一方主键,将外键设为唯一unique(一对一,一个值对应一个值,一个值不会对应两个值)

就是加上外键约束,然后将外键那个字段加上unique

多对多

第三张中间表,至少两个外键,关联两方主键

多表查询

笛卡尔积

​ 比如A表和B表查询,select * frome A,B,会返回所有组合情况,这就是笛卡尔积,但实际有很多是无效的所以要加条件。

X 1 1 AA
X 1 2 BB
1 AA X 1 3 CC
X 1 2 BB X 1 4 DD
Y 2 3 CC Y 2 1 AA
4 DD Y 2 2 BB
Y 2 3 CC
Y 2 4 DD

加上条件

X 1 1 AA
1 AA
X 1 2 BB
Y 2 3 CC
4 DD Y 2 2 BB
  • 连接查询
    • 内连接
      • 隐式内连接
        • from 表1,表2 where
      • 显式内连接
        • from 表1 [inner] join 表2 on
    • 外连接
      • 左外连接
        • left [outer] join
      • 右外连接
        • right [outer] join
  • 子查询
    • 外部语句可以是insert/update/delete/select
    • 标量子查询 >=<
    • 列子查询 一列多行,比如id列 in, not in
    • 行子查询 一行多列,比如一条相同记录 =,<>,in,not in
      • 组合值 (id,name)=(1,zhangsan)
    • 表子查询 比如对一个表先筛选再和另一个表对应 in或者临时表

MySQL 练习实践 (w3ccoo.com)

事务

一组操作的集合,把操作作为整体像系统提交,所以要么同时成功要么同时失败。

MYSQL中每条DML语句隐式提交事务,所以可能有问题。

start transaction;/begin;
commit; -- commit之前别的窗口是不变的,因为相当于不同的事务,事务隔离
rollback;

特性

  • 原子性 要么全部成功,要么全部失败
  • 一致性 数据保持一致
  • 隔离性 保证事务不受外部并发影响
  • 持久性 事务一旦提交或回滚,对数据改变是永久的

ACID

索引

优化

  • 索引
  • SQL优化
  • 分库分表

create index 索引名 on 表名(字段名);

全表扫描->索引

索引是帮助高效获取数据的数据结构

默认B+Tree结构,多路平衡搜索树

create [unique] index 索引名 on 表名(字段名,...)-- 主键默认主键索引
-- unique唯一约束其实就是添加了索引

show index form 表名;

drop index 索引名 on 表名;

Mybatis

持久层(Dao)框架,简化JDBC开发

Dao层@Repository->@Mapper

@SpringBootTest整合单元测试注解

配置sql提示

右键show context actions->inject language->mysql

JDBC

Java DataBase Connectivity

操作关系型数据库的一套API,一套接口

注册驱动
获取连接对象
创建Statement对象执行SQL返回结果
封装结果数据
释放资源

数据库连接池

数据库连接池

标准接口:DataSource

Connection getConnection() throws SQLException;

Hikari追光者 Druid德鲁伊 DBCP C3P0

lombok

@Getter/@Setter

@ToString

@EqualsAndHashCode

@Data = @Getter/@Setter + @ToString + EqualsAndHashCode

@NoArgsConstructor

@AllArgsConstructor

操作

删除

#{} 占位符

删除 预编译SQL

预编译SQL

  • 性能更高
  • 更安全,防止SQL注入

{SQL语法解析检查->优化SQL->编译SQL}(缓存)->执行SQL

预编译就不用每个不同变量都经过缓存了->性能更高

  • #{…} 参数传递
  • ${…} 拼接SQL

新增

主键返回

@Options(keyProperty = "id", useGeneratedKeys = true)

更新

update 表名 set …

实体类

查询

数据封装:

  • 实体类属性名和数据库查询返回字段名不一致不能自动封装

解决方案:

  1. 起别名和实体类属性一致
    • 改SQL语句,加上起别名
  2. 注解
    • @Results({@Result(column=“”,property=“”),…})
  3. 开启驼峰命名映射
    • mybatis.configuration.map-underscore-to-camel-case=true

条件查询模糊匹配

like '%zzz%'
->
like '%${name}%' -- 因为%%不能用#{}
-- 效率低,SQL注入>
like concat('%',#{name},'%')
-- concat拼接

1.x版本/单独使用mybatis

需要@Param注解

XML

规范:

  • 同包同名
  • namespace和mapper接口全限定名一致,也就是路径名 (namespace)
  • id与方法名一致(id),返回类型一致(resultType)

动态SQL

<if>拼接SQL,使用test属性条件判断,例如<if test="name!=null">
<where>动态,会去掉多余的andor
<set>去掉多余逗号,update中使用
where子句
->
<where>
    <set>
    <if>
    </if>
    </set>
</where>


<forEach>
-- collection:遍历集合
-- item:元素
-- separator:分隔符
-- open:遍历开始SQL片段
-- close:遍历结束SQL片段
<forEach collection="ids",item="id",separator=",",open="(",close=")">
#{id}
</forEach>


<sql><include>
抽取可重用SQL片段和引用
<sql id="selectAll">
...
</sql>
<include refid="selectAll"/>

开发规范

Restful

REST REpresentational State Transfer 表示性状态转换,软件架构风格

REST风格

  • URL定位资源
  • HTTP动词描述操作

统一响应结果Result

日志记录

private static Logger log = LoggerFactory.getLogger(DeptController.class);
log.info("这个接口查询全部部门数据");
->
@Slf4j
log.info("...");

controller=>service=>impl=>mapper=>xml/注解

查询结果封装Bean

//分页查询

controller层
->传参,@RequestParam,返回封装Bean
service层
->调用实现方法获得返回值,new Bean对象
mapper层
->sql

//分页插件PageHelper
引入依赖
mapper层不用limit
service层
->
设置分页参数PageHelper.startPage(page,pageSize)
执行查询List<Emp> empList = empMapper.list();Page<Emp> p = (Page<Emp>) empList
封装PageBean对象PageBean pageBean = new PageBean(p.getTotal(),p.getResult())
    

文件上传三要素

Web后端开发_第1张图片

MultipartFile接收

Web后端开发_第2张图片

文件上传–阿里云OSS

配置文件

配置优先级

properties > yml > yaml

除了配置文件另外两种,命令行 > Java系统属性 > 配置文件

Java系统属性

-Dserver.port=9000

命令行参数

–server.port=9000

打包后指定

java -Dserver.port=9000 -jar … --server.port=10010

参数配置化

配置在appication.properties

通过@Value(“${aliyun.oss.endpoint}”)注解用于配置属性注入

yml配置文件

  1. 大小写敏感
  2. 数值前要有空格作为分隔符
  3. 缩进层级关系,不能tab(idea会自动转化)
  4. 相同层级的元素左侧对齐就可以
  5. #注释

数据格式

# 定义对象/Map集合
user:
	name: Tom
	age: 20
	address: beijing
	
# 数组/List/Set集合
hobby:
	- java
	- C
	- game
	- sport
	
# 数据库
spring:
	datasourse:
		driver-class-name:
		url:
		username:
		password:
	
# Mybatis配置
mybatis:
	configuration:
		log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
		map-underscore-to-camel-case: true

@ConfigurationProperties

@Value->注入类,批量的将外部属性配置注入到bean对象的属性中

@Data
@Component
@ConfigurationProperties(prefix="aliyun.oss")
public class AliOSSProperties{
    //对应配置项
}

依赖spring-boot-configuration-processor可选

登录校验

统一拦截->Filter过滤器/Interceptor拦截器

登录标记->会话技术

会话技术

会话

会话跟踪

会话跟踪方案:

  • 客户端会话跟踪技术:Cookie
  • 服务端会话跟踪技术:Session
  • 令牌技术

Cookie

  • Cookie
  • Set-Cookie
//设置Cookie
public Result cookie1 (HttpServletResponse response){
    response.addCookie(new Cookie("name","value"));
    return Result.success();
}

//获得Cookie
public Result cookie2 (HttpServletRequest request){
    Cookie[] cookies= request.getCookies();
    for(Cookie cookie:cookies){
        if(cookie.getName().equals("name")){
            System.out.printlan("name"+cookie.getValue());
        }
        return Result.success();
    }
}

跨域区:
    协议,IP/域名,端口
    

JWT令牌

JSON Web Token

简洁的、自包含的格式

组成:

  1. Header,记录令牌类型,签名算法等 base64
  2. Payload,有效载荷
  3. Signature,签名,防止token被篡改、确保安全性
//生成
Jwts.buidler()
    .signWith(SignatureAlogorithm.HS256,"test")
    .setClaims(test)
    .setExpiration(new Date(System.currentTimeMillis()+1000))
    .compact();

//解析
Jwts.parser()
    .setSigningKey("test")
    .parseClaimsJws("")
    .getBody();


//登录后下发
Map<String,Object> claims = new HashMap<>();
claims.put("id",e.getId());
String jwt = JwtUtils.generateJwt(claims);


过滤器Filter

Servlet Filter Listener JavaWeb三大组件

@WebFilter(urlPatterns="/*")//Filter类

public void init(FilterConfig filterConfig) throws ServletException{
    Filter.super.init(filterConfig);
}
public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain){
    System.out.println("");
    //放行
    chain.doFilter(request,response);
}
public void destroy(){
    Filter.super.destroy();
}

@ServletComponentScan //启动类

执行流程

​ 放行前

​ 放行

​ 放行后(会回到Filter)

拦截路径

​ /login

​ /login/*

​ /*

过滤器链

​ 排序按类名排序

@Override
@WebFilter(urlPatterns="/*")
public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) throws IOException{
    HttpServletRequest req = (HttpServletRequest) request;
    HttpServletResponse resp = (HttpServletResponse) response;
    
    //拿URL
    String url = req.getRequestURL().toString();
    log.info("",url);
    //登录就放行
    if(url.contains("login")){
        log.info("");
        chain.doFilter(request.response);
        return;
    }
    //拿jwt
    String jwt = req.getHeader("token");
    //没登陆
    if(!StrignUtils.hasLength(jwt)){
        log.info("");
        Result error = Result.error("NOT_LOGIN");
        String notLogin = JSONObject.toJSONString(error);
        resp.getWriter().write(notLogin);
        return;
    }
    //解析
    try{
        JwtUtils.parseJWT(jwt);
    }catch(Exception e){
        log.info("");
        Result error = Result.error("NOT_LOGIN");
        String notLogin = JSONObject.toJSONString(error);
        resp.getWriter().write(notLogin);
        return;
    }
    //解析通过
    lgo.info("放行");
    chain.doFilter(request,response);
    
}

拦截器Interceptor

HandlerInterceptor

ctrl+O

@Component
public class LoginCheckInterceptor implements HandlerInterceptor{
    @Override
    public boolean preHandle(HttpServletRequest req,HttpServletResponse resp,Object handler)throws Exception{
        System.out.println("目标资源方法执行前");
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest req,HttpServletResponse resp,Object handler,ModelAndView modelAndView){
        System.out.println("目标资源方法执行后");
    }
    @Override
    public void afterCompletion(HttpServletRequest req,HttpServletResponse resp,Object handler,Exception ex){
        System.out.println("视图渲染完毕后");
    }
    
}


@Configuration
public class WebConfig implements WebMvcConfigurer{
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    publci void addInterceptores(InterceptorRegistry registry){
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");
    }
    
}

拦截路径

registry.addInterceptor(loginCheckInterceptor).addPathPatterns(“/**”).excludePathPatterns(“/login”);

  • /*
  • /**
  • /depts/*
  • /depts/**

执行流程

Filter->DispatchServlet(Tomcat识别)->Interceptor->Controller层

区别:

  • 接口规范不同:Filter,HandlerInterceptor
  • 拦截范围不同:Filter所有,Interceptor只拦截Spring环境
放行通过return true,不放行false

异常处理

全局异常处理器

现在前端错误信息是JSON没法解析,异常处理后前端可以收到

@RestControllerAdvice=@ControllerAdvie+@Resposnebodys

@RestControllerAdvice
public class GlobalExceptionHandler{
    @ExceptionHandler(Exception.class)//捕获所有异常
    public Result ex(Exception ex){
        ex.printStackTrace();
        return Result.error("111");
    }
}

事务管理

@Transactional

方法 类 接口

日志

logging:
	level:
		org.springframework.jdbc.support.JdbcTransactionManager: debug

@rollbackFor

默认异常回滚只回滚RuntimeException

rollbackFor控制出席那何种异常属性回滚事务

@Transactional(rollbackFor=Exception.clss)

@propagation=Propagation.?

事务传播行为:当一个事务方法被另一个事务方法调用,事务方法如何进行事务控制

  • REQUIRED 有则加入,无创建新事务
  • REQUIRED_NEW 都创建新事务
  • SUPPORTS
  • NOT_SUPPORTED
  • MANDATORY
  • NEVER

finally{}不论是否异常都记录日志

  • REQUIRED 有则加入,无创建新事务 大部分可以
  • REQUIRED_NEW 都创建新事务 当不希望事务之间相互影响时可以使用该传播行为

AOP

Aspect Oritented Programming

面向特定方法编程

动态代理

记录日志、权限控制、事务管理、…

  • 代码无侵入
  • 少重复
  • 提高开发效率
  • 维护方便

入门程序

PrceedingJoinPoint joinPoint

@Component
@Aspect//aop类
public class TimeAspect{
    
    @Around("execution(*  com.itheima.service.*.*(..))")//用在哪个类,第一个*是返回类型,表示返回任意类型
    public Object recordTime(ProceedingJoinPoint joinPoint){
        long begin = System.currentTimeMillis();
        
        Object result = joinPoint.proceed();
        
        long end = System.currentTimeMillis();
        log.info(joinPoint.getSignature+"{}",end-beegin);
        
		return result;
    }
}

核心概念

  • 连接点 JoinPoint 可以被AOP控制的方法
  • 通知 Advice 共性功能,方法
  • 切入点 PointCut 匹配连接点的条件,通知只在切入点方法执行时应用 @Around
  • 切面 Aspect 通知和切入点的对应关系(通知+切入点)
  • 目标对象 Target 通知所应用的对象

AOP执行流程

生成对应代理对象,过程注入代理对象

CGLIB动态代理

通知类型

  • @Around 环绕通知
  • @Before 前置通知
  • @After 后置通知,也叫最终通知
  • @AfterReturning 返回后通知
  • @AfterThrowing 异常后通知

声明切入点

@Pointcut("...")
private void pc(){}

@Around("pt()")

通知顺序

  1. 类名字母
    • 目标方法前,靠前先执行
    • 目标方法后,靠前后执行
  2. @Order
    • 目标方法前,数字小先执行
    • 目标方法后,数字小后执行

切入点表达式@Pointcut(“”)

  • execution(…) 根据方法的签名来匹配
    • execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
    • * 单个任意符号
    • … 多个连续的任意符号
  • @annotation(…) 根据注解匹配
    • 自定义注解
      • public @interface MyLog
      • @Retention(RententionPolicy.RUNTIME)
      • @Target(ElementType.METHOD)
    • @annotation(“com.itheima.aop.MyLog”)

连接点

JoinPoint

  • @Around ProceedingJoinPoint
  • 其他四种通知 JoinPoint

JoinPoint 是 ProceedingJoinPoint 父类

案例 记录日志

//自定义注解
@Retention(RententionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log{
    
}

//切面类
@Slf4j
@Component
@Aspect
public class LogAspect{
    
    @Arount("@annotation(...)")
    public Object recordLog(ProceedingJoinPoint joinPoint){
        
        String jwt = request.getHeader("token");
        Claims claims = JwtUtils.parseJWT(jwt);
        Integer operateUser = (Integer) claims.get("id");
        
        LocalDateTime operateTime = LocalDateTime.now();
        
        Stirng className = joinPoint.getTarget().getClass().getName();
        
        String methodName = joinPoint.getSignatur().getName();
        
        Object[] args = joinPoint.getArgs();
        String methodParams = Arrays.toString(args);
        
        long begin = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        
        String returnValue = JSONObject.toJSONString(result);
        
        Long costTime = end - begin;
        
        
        OperateLog operateLog = new OperateLog(...);
        operateLogMapper.insert(operateLog);
        log.info("{}",operateLog);
        
        return result;
    }
}

bean管理

先自动注入

bean系统创建是和类一样首字母小写

  1. 名称获取
    • DeptController bean = (DeptController) applicationContext.getBean(“deptController”);
  2. 类型获取
    • DeptController bean = applicationContext.getBean(DeptController.class)
  3. 名称类型获取
    • DeptController bean = applicationContext.getBean(“deptController”,DeptController.class)

bean作用域

  • singleton 单例
  • prototype 每次使用创建新的
  • request 每个请求
  • session 每个会话
  • application 每个应用

@Lazy 使用的时候才初始化创建Bean,默认启动就创建

@Scope(“…”)

第三方bean

@Bean

@Bean //将当前方法的返回值对象交给IOC容器管理,成为IOC容器bean
public SAXReader(){
	return new SAXReader();    
}

//配置类统一管理
@Configuration
public class CommonConfig{
    @Bean //将当前方法的返回值对象交给IOC容器管理,成为IOC容器bean
    //通过name/value指定,默认方法名
    public SAXReader saxReader(){
        return new SAXReader();    
    }
}

//如果要注入依赖,直接在bean定义方法设置形参即可,容器会注入

SpringBoot原理

起步依赖

​ spring-boot-starter-web集成了web开发的常见依赖

​ 通过依赖传递实现

自动配置

​ spring容器启动后,一些配置类、bean对象自动存入了IOC容器

自动配置原理

方案一:

引入依赖,然后通过@ComponentScan扫描

方案二:

@Import导入,使用@Import导入的类会被Spring加载到IOC容器中

@Import({…})

  • 导入普通类

  • 导入配置类

  • 导入ImportSelector接口实现类

  • 封装@EnableHeaderConfig

    • @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE)
      @Import(MyImportSelector.class)
      public @interface EnableHeaderConfig{
          
      }
      

源码跟踪

@SpringBootApplication

->

@SpringBootConfiguration

@ComponentScan

@EnableAutoConfiguration

->

@Import(AutoConfigurationImportSelector.class)

交给IOC容器管理

->

String[] selectImports(…)方法,加载全类名

->

spring.factories旧版本

spring目录下org.springframework.boot.autoconfigure.AutoConfiguration.imports

->

全类名

XXXAutoConfiguration

不是所有都注册到IOC

@ConditionalOnMissingBean 按条件装配

@Conditional 父注解

  • @ConditionalOnClass
  • @ConditionalOnMissingBean
  • @ConditionalOnProperty

案例-自定义starter起步依赖

依赖管理功能

自动配置功能

aliyun-oss-spring-boot-starter

  • aliyun-oss-spring-boot-starter.iml
  • pom.xml
    • com.aliyun.oss
    • aliyun-oss-spring-boot-autoconfigure

aliyun-oss-spring-boot-autoconfigure

  • AliOSSProperties
  • AliOSSUtils
  • AliOSSAutoConfiguration
  • 依赖

META-INF/spring/xxx.imports

  • 全类名

总结

  • JavaWeb 过滤器,Cookie,Session
  • 解决方案 JWT,阿里云OSS
  • SpringMVC 接受请求,响应数据,拦截器,全局异常处理
  • Spring framework IOC,DI,AOP,事务管理
  • Mybatis
  • SpringBoot

SSM框架->SpringBoot

Maven高级

分模块设计与开发

继承与聚合

继承关系

  1. 父工程,设置打包方式pom
  2. 子工程继承关系
  3. 父工程配置依赖

版本锁定

父工程

子工程不用标注version使用

自定义属性

<properties>
	<lombok.version>1.18.24</lombok.version>
</properties>

聚合

  • 聚合
    • 将多个模块组织成整体,同时进行项目的构建
  • 聚合工程
    • 一个不具有业务功能的工程(父工程)
  • 作用
    • 快速构建项目,直接在聚合工程上构建即可

​ …

私服

负载均衡

你可能感兴趣的:(java)