SSM实操笔记(自留)

单元测试;在test包下闯进一个类,类名前@SpringBootTest,方法名前@Test

@Autowired只能放在构造方法中(与类名同名的方法)而不能放在其下的方法中

1.0说白了,组件就是类的对象,作用就i是将组建的实例化对象创建然后注入到需要使用的类中

1.1新版idea没有脚手架,直接创建一个maven
项目,保留src和pom,在pom中设置打包方式为pom,然后创建moudle,选择springboot即可,
SpringXXXApplication.java为入口文件,尝试运行成功

1.2尝试lobok,使用@Data不用再写setter,在pom中添加依赖,注意在使用的Java类中import一下

1.3applicationcontext上下文对象,即是spring容器,在容器中插入自己的组件bean,注意方法名为组件名,不要写到main中。从容器中获取组件(所有):ioc.getBeanDefinitionNames();

手动实例化并加入到容器中@Bean("hhhh")
    public student student(){
        return new student();
    }

1.4运行窗口内ctrl+f 可以寻找输出信息

1.5getBean若果有多个同名组件,是谁先写取出谁,最好同时getbean(类名加组件)来获取组件,组件的创建(即实力话Java类)优先于容器的创建
组件时单实例的,每次get,只会创建一个对象

1.6   写在配置文件中的<Bean>,现在可以写在一个配置类中@Configuration声明一下这个类是配置类(其中所有的注解可以被扫描到并起作用·),然后在这个类中@Bean手动实例化并加入到容器,或者其他注释将组件注册

1.7  @Component标记一个POJO类注册为组件

MVC分层组件注释(@controller等)写在类的上一行
        @Service
        class userservice {
这些注解直接写在类上无需手动实例化会自动创建并加入都容器中,想用就get

1.8 在入口类的main前@ComponentScan组件扫描,扫名路径中的的所有包下的注解

1.9 导入第三方的类的对象(jar包,只读不能改),只能用@import(类名)导入后自动创建组件对象
注意的是所有组件,只需要导入一次就全局使用,建议创建一个AppConfig,其中添加扫包@ComponentScan和@import至于其中

1.10多例模式@Scope("prototype")  @Scope("singleton")单例模式(写在类名前)
单例:每次getBean同一个组件名
ioc.getBean("hhhh");ioc.getBean("hhhh");
获取的是同一个对象;且单实例对象在容器创建之前就创建完成了(调用构造函数)

多例:不同对象,获取时才创建对象


1.11@Lazy //懒加载写在类(组件类)名前
社么时候get什么时候创建,不get就不实例化(只有单例模式才能用)

1.12  Bean工厂:
对于没有@Bean或@Component或@import的类(组件),get是找不到的

public class BYD implements FactoryBean<未注册的类名>{
@Override
    public 类名 getObject() throws Exception {
        手动实例化目标品类对象
    }

    @Override
    public Class<?> getObjectType() {
        return 类名;
    }

    @Override  //指定这个工厂生产的组件是否是单例模式
    public boolean isSingleton() {
        return TURE;
    }
}
这个组件就会由这个工厂创建并注册为组件(加入到容器中)可以get

1.13  条件注册(满足条件时才注册为组件)
@Conditional({条件类类名})//写在组件前或整个类前,满足条件后才会加载这个组件。需创建一个条件类实现
implements Condition接口
需复写方法public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata)          return true;//返回ture则满足条件
快捷键ctrl+H快速当如常用条件注解如@conditonalOnMissingBean(组件名)如果没有当前“组件名”的组件,则将其下面的组件 注册为组件


2.1  @Autowired依赖注入,即getBean,在需要的类中获取组件: @Controller
public class UserContrl {
    @Autowired
    Userservic1 userservic1;
}  

2.2  需要用组建的类中:@Qualifier("maoshi")//2.2 精准注入,根据注册时的组件名注入
配置类中:@Primary  // 当有多个相同类型的组件时,默认装配的是id为shit的组件,加了@Primary注解后,默认装配的是id为maoshi的组件


2.3  @Resource也能自动注入,是Java定义的,更规范

2.4  想使用的类中用构造方法注入,强制创建目标类对象

2.5  @value:
写在POJO类的属性名前实现 值的注入
public class Shit {
    @Value("大狗shi")
    private String name;
取值:
@Value("${shit.name:morenzhi}")
String 变量名;
 //$去配置文件中取值注入值给下面的变量名 取不到则使用冒号后的只作为默认值
@Value("#{ java语句  })//写spEL表达式,内部代码执行完后取出返回值

2.6标准化,一个类若需要配置属性,在resources中创建一个xxx.properties
经典:数据库连接相关的数据写在db.properties
欲导入这些属性,写在类名前
@PropertySource("classpath:shi.properties") 指定配置文件的路径
@PropertySource("classpath:*")  从所有路径下找·包括外部的jar也能被扫描


2.7  获取外部资源(图片等)放置到resources中
使用时:
        File flie = ResourceUtils.getFile("classpath:屏幕截图 2024-09-01 220309.png");

2.8  多环境@Profile(环境标识),写在bean组件前,
在application.properties中spring.profiles.active=data2,激活某个表示,从而使@Profile以下的代码生效
     

3.1    组件的生命周期
@Bean(initMethod = "init",destroyMethod = "destroy")
init,destroy分别是组件类中定义的方法,这样写之后,先执行组件的的构造函数,执行其他的依赖注入@Auto。。。。;再执行initMethod指定的方法,执行destroyMethod指定的方法,最后关闭容器


3.2  俩个生命周期接口周期接口:
实现InitializingBean接口,类中定义afterPropertiesSet()方法,在依赖注入以后@Auto,在组件注入@Bean(initMethod = "init"之前
实现DisposableBean接口,destroy()方法,在@Bean(destroyMethod = "destroy")之前运行

3.3  写在方法前
@PreDestroy 指定的方法-->   “实现DisposableBean接口的类中的destroy()方法”-->在@Bean(destroyMethod = "destroy")

@PostConstruct 在依赖注入之后,InitializingBean接口定义afterPropertiesSet()方法之前

3.4  具有返回值的生命周期函数,可以动态修改 
实现implements BeanPostProcessor接口,
postProcessAfterInitialization(Object bean, String beanName)
postProcessBeforeInitialization(Object bean, String beanName){bean.setname....}
方法中可以直接操作bean这个对象对拦截的组件中的属性修改

顺序:构造方法-->
    依赖注入@Auto-->
    后置处理器(初始化之前后置处理)postProcessBeforeInitialization方法-->
    @PostConstruct(构造器的生命感知)标记的方法-->
    InitializingBean接口中定义的afterPropertiesSet()方法-->
    @Bean(initMethod = "init"中的init()初始化Bean方法-->
    后置处理器(初始化之后的后置处理)postProcessAfterInitialization(Object bean, String beanName)  运行之前最后的一个处理-->
    @PreDestroy 指定的方法-->
     实现DisposableBean接口的类中的destroy()方法”-->
    在@Bean(destroyMethod = "destroy")真正销毁

    
    
4.1  AOP切面注入方法:
细节:   ..的作用:在参数上表示任意数量,任意类型的参数
            在包路径中匹配任意数量,任意名称的层级
    *  匹配任意字符
切入点表达式最简单写法* *(..)
切面类@Aspect   定义要插入的方法,方法前@Before@After等注解表明何时插入,@Before("execution()或arge()")指定插入的位置(方法名)
 

4.2  常用切点表达式:
    execution(方法的全限定签名)(包。类。方法名(参数列表));简写:“返回值类型 方法名(参数一类型,参数二类型)”

    args(参数一类型,参数二类型)  :匹配所有参数类型数量匹配的方法


4.3  组件在容器中并不是她本身的实例对象,而是创建的XXXSpringCGLIB代理对象

4.4通知的执行顺序
        // 正常情况: 前置通知 目标方法 后置通知 
        // 异常情况: 前置通知 目标方法 异常通知  后置通知  

4.5  连接点信息
joinPoint

4.6  @Pointcut("execution(int add(int,int))") 定义一个切入点表达式可以多次被使用

4.7  细节
//如果有多个切面切同一个方法,
1的前置通知,2的前置通知,目标方法执行,2的后置通知,1的后置通知
@Order用来调整优先级@Order(1)//如果有多个切面切同一个方法   数字越小优先级越高


4.8 环绕通知
    //环绕通知 通知方法返回值必须是object,连接点信息joinPoint
    // 目标方法不会自动执行
 环绕通知:可以修改目标方法,参数,返回值等
细节!:如果环绕通知时接收异常;catch (Exception e){
                System.out.println("环绕通知,异常信息:" + e.getMessage());
则外部的其他异常通知就不会显示了,走正常的后置通知,因为接收后没有抛出异常。
所以!环绕通知最好抛出自己的异常;catch (Exception e){
                System.out.println("环绕通知,异常信息:" + e.getMessage());
                throw e;

5.1使用spring框架连接数据库,在pom中点击依赖设置,加入sql中的JDBC ,Spring data jdbc,mysql driver三个依赖
在配置文件application.properties中配置连接信息
    spring.datasource.url=jdbc:mysql://localhost:3306/数据库名
    spring.datasource.username=wjj
    spring.datasource.password=330204
    spring.datasource.driver-class-                name=com.mysql.cj.jdbc.Driver

使用时:@Autowired
    DataSource dataSource;
    void contextLoads() throws Exception {
        Connection conn = dataSource.getConnection(); 获取链接

5.2  JdbcTemplate对象简化数据库操作:
在Spring框架中,使用JdbcTemplate可以简化数据库操作,并且不需要手动连接数据库。JdbcTemplate会自动从application.properties或application.yml文件中读取数据库连接参数,并建立连接。
在你的代码中,@Autowired注解用于自动注入JdbcTemplate实例到BookDao类的jdbcTemplate字段中。一旦注入完成,你就可以通过这个jdbcTemplate对象来调用各种数据库操作方法

int res = jdbcTemplate.update("insert into account values(?,?,?)","1","2","3");

5.3 尝试查询:
jdbcTemplate.queryForObject(sql,new BeanPropertyRowMapper<>(Book.class),id);
        //queryForObject方法 执行查询,返回一个对象,参数为:sql语句,new BeanPropertyRowMapper<>(Book.class)返回的目标对对象类型,查询参数

发现的错误:import自动导入时导入了底层的Book类同名原因


5.4  尝试插入图书
细节:book.setPrice(new BigDecimal(200));
BigDecimal类型的参数必须这样创建才行

5.8综合测试,结账

5.9  注册为事务
在入口文件Practice03SqlShiwuApplication.java中;
@EnableTransactionManagement11 //开启事务管理
在需要事务管理的方法前
@Transactional表示开启事务

5.10 传播行为,事务超时时间,隔离级别
@Transactional(timeout = )  事务超时时间超时就回滚单位是秒
@Transactional(readOnly =ture)

5.11  隔离级别
@Transactional(isolation=Isolation.READ_UNCOMMITTED)  //读未提交,会产生脏读,不可重复读(因为多次读取数据时结果可能不同),幻读
@Transactional(isolation=Isolation.READ_COMMITTED)  //读已提交,sql语句提交commit命令后才能查询到数据,默认
//        @Transactional(isolation=Isolation.REPEATABLE_READ)  //可重复读,无论外面是否修改数据,每次读取的数据都是一样的
//        @Transactional(isolation=Isolation.SERIALIZABLE)

5.12 传播行为

    REQUIRED(0),需要进行事务管理,如果外部方法存在事务,则和外部使用同一个,即外部出现异常,自己即使没有异常也一同回滚,如果不存在,则用自己的新事务
    SUPPORTS(1), 支持事务管理,如果外部方法不存在事务,则非事务运行,无论是否异常的都不会回滚
    MANDATORY(2), 强制,外部方法有事务在用同一个,如果没有,直接抛异常不运行
    REQUIRES_NEW(3),  总是需要新的,无论外部方法有没有事务,都使用自己的事务,即外部回滚,自己只要不出错就不会滚
    NOT_SUPPORTED(4),不支持,自己不会事务运行,如果外部方法是事务运行则暂停,自己运行完在运行外部的
    NEVER(5),  拒绝,自己始终不事务,如果外部有事务,则直接报错
    NESTED  保存点,一个业务方法中有一系列dao中的方法,只回滚到最后一个正确执行的方法处,往后的所有回滚


5.13传播行为细节:
内部的方法(即使REQUIRES_NEW)如果异常,异常信息会向上抛出,外部的也会回滚,外部方法内的所有REQUIRED的方法就算也没有出错也回滚,但是外部方法的所有REQUIRES_NEW方法不回滚


如果和外部方法用一个事务即REQUIRED
则在本方法中的属性(超时时间那里)全部失效


6.1  前后端分离最重要要的是序列化反序列化,服务器端将字符串转化为对象(反序列化),客户端需要将对象转化为字符串(序列化)
创建springmvc项目,勾选web-->spring web实际上是spring boot做的

6.2  MVC在学习时@RequestMapping("/hello")
    public String hello() {

        return "hello";
    }
return hello长指的是jsp页面,现在前后端分离,追求的是后端只返回数据
送so,在方法名前@ResponseBody  //6.2将返回的数据值放到响应体中返回欸前端

新!:@RestController
同时实现上面所说的两个注解,标在控制器类类名前

6.3  在springmvc项目里的resources\application.properties配置文件中:server.port=8888  可更改TomCat的端口号


6.4
通配符;
    *匹配任意数量字符(包括0个),不能匹配多个路径(不能匹配hello/xxx)

    **匹配任意多层路径
@RequestMapping("/hello/**")
    ?有且只能有匹配一个任意的字符    

存在通配符的路径若能和精确的路径重合,精确的优先
@RequestMapping("/hello")  //优先
@RequestMapping("/hell?")

精确度:?》*
@RequestMapping("/hell?") 优先
@RequestMapping("/hell*")

精确拦截不能两个完全一样的
@RequestMapping("/hello")//这句话不能同时存在两个

6.5    请求中限制
@RequestMapping(value = "/hello",method = RequestMethod.POST)  只允许post请求
@RequestMapping(value = "/hi",params = {"name","age=1"})  //6.5 测试请求中限制请求体中必须有name和age两个参
@RequestMapping(value = "/hi2",headers = {"shit!=2","shit1=1"})
    //6.5 测试请求中限制请求头不能是1  或者没有shit1这个请求头 // {"shit1=1"}  必须有请求头shit1,且值为1,在postman里测试
 @RequestMapping(value = "/hi3",consumes = {"application/json"})
//6.5 测试请求中限制请求的数据必须是json类型

form表单
enctype="application/x-www-form-urlencoded
表单的数据编码格式

6.6  url中最后出现的"#xxx"表示页面锚点,不参与访问,仅表示当前页面的不同段落或不同节点

6.7 
实验1,使用表单的提交参数的id值接收表单的提交的参数
注意方法接收的参数名必须与表单中的参数名一致

实验二:@RequestParam(value = "password",required = false,defaultValue = "123") String pws,
指定接收前端哪个参数名password的参数赋值给方法内的参数pws
required = false,可以没有这个参数
defaultValue = "123" 这个参数可以不带,当前端提交请求时没有带上这个参数(空值),默认将123这个值赋值给方法内参数pws
实验2-10
 实验8,测试文件上传,默认限制上传大小,配置文件中修改:
spring.servlet.multipart.max-file-size=1GB  //文件的大小
spring.servlet.multipart.max-request-size=1GB//单次请求的大小


7.1  测试返回(响应数据)
实验一;将POJO对象转为JSON并返回给前端
实验二;测试文件上传下载, 都是固定格式
注意两点
    中文乱码 : filename=1.png 指定的文件名为中文时出现乱码 解决方法:URLEncoder.encode("1.png","UTF-8")
        响应体 : ResponseEntity<byte[]>  这样返回太大了应该返回InputStreamResource,将整个字节流文件再封装一下

7.2  模板引擎代替jsp,写在resources/templates中写html
引入Template Enginess  >  Thymeleaf
封装了视图解析器,直接return 页面名就能实现跳转(只不过不支持jsp了,只能匹配html)


两种动态获取数据:
1  <h3>用户名;<span th:text = "${username}"></span></h3>
//th属性可以标记其他属性,也能动态修改
<span th: id= "${id}"></span>

 2  <h3>密码;[[${password}]]</h3>


7.3  页面跳转,前后端交互总结

@RequestMapping  拦截后处理方法的
返回值是String 默认实现页面天跳转

@ResponseBody(@RestController 实现@Controller+@ResponseBody)
表明返回给前端以json数据(可以返回pojo类对象,会自动转化为json数据返回)

@RequestMapping  拦截后处理方法的
返回值是ResponseEntity  表示自定义响应,需要自己填充响应头和响应体

@RequestMapping  拦截后处理方法的
返回值是DeferredResult 表示返回异步响应

@RequestBody;
@RequestMapping("/handle07")  // 实验7,测试接收JSON类型的数据(前端通过ajax请求),取出请求体中的JSON转化为POJO对象 @RequestBody(自动反序列化)
    public String hello7(@RequestBody Person person

8.1
RESTful架构风格  简化访问路径
就是将访问的url命名不再使用动词表示业务逻辑,而是直接命名为资源名和操作名

接口:在web中,调用后端服务器中的某个功能实现业务逻辑两种方法;
API;这里的API指前端发出一个url访问请求,后端Controller控制这个访问@RequestMapping去匹配访问路径,匹配这个路径的方法在web开发中就是接口,说白了就是@PostMapping("/employee")

SDK;就是直接将写好的服务(jar包0部署包本地,直接调用


@PathVariable获取路径变量
问题一,区分路径变量和请求参数
在/get/{id}这个URL中,{id}就是一个路径变量,是由前端的表单的action决定的
@PathVariable注解来获取
问题2,路径变量其他用法
@RequestMapping(value = "/employee/{*id}
*匹配若干曾的路径并封装为一个;/employee/1/2/3 --->识别为”123“


在/search?name=John这个URL中,name=John就是一个请求参数,是由前端的表单中记录的,@RequestParam注解来获取


尝试Restful风格,根据不同的请求方式method,匹配不同的处理方法;控制器中;

@RequestMapping(value = "/employee/{id}",method = RequestMethod.GET)


@RequestMapping(value = "/employee/{id}",method = RequestMethod.DELETE)

路径都是一样的,去对应要修改的资源名,请求方式不一样从而区分

优化;提供了一种新注解

实现只接受get请求
  @GetMapping("employee/{id}")
@DeletMapping等等


8.2POST请求,restful风格个中用于添加操作,传入一个OJO对象,在请求体body中raw中,封装jso格式的数据
PUT 去对应修改请求

8.3  企业级开发中后端发回给前端一个固定的json(无论什么操作)
创建一个共用类common.R
包括code状态码,msg信息,data数据

控制器类的处理方法中:
return R.ok()  如果需要传参给前端(查询出一个POJO对象给前端)则携带参数:return R.ok(POJO对象) ,或者传入自定义的状态码

在控制器的处理方法上,返回值类型选择为R

这样以后返回给前端的数据:
{
  "code": 200,
  "msg": "Success",
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "email": "john@example.com"
    },
    {
      "id": 2,
      "name": "Jane Smith",
      "email": "jane@example.com"
    }
  ]
}


8.4查询所有
query


8.5跨域问题

前端页面`localhost:80`访问后端`localhost:8080`会产生跨域问题。
场景描述
假设你正在开发一个Web应用,其中前端页面运行在`localhost`的80端口上,而后端API服务运行在`localhost`的8080端口上。你的前端页面需要通过AJAX请求向后端API获取数据。


步骤和解释


• 前端页面加载:

• 用户打开浏览器,访问`http://localhost:80`。

• 浏览器加载页面,这个页面是运行在80端口上的前端应用。


• 发起AJAX请求:

• 页面中的JavaScript代码尝试发起一个AJAX请求,目标是`


• 浏览器检查同源策略:

• 浏览器检查这个AJAX请求的目标地址(`

• 同源策略要求协议、域名和端口号必须完全相同。在这个例子中,虽然协议(HTTP)和域名(localhost)相同,但端口号不同(80和8080)。


• 跨域问题发生:localhost:xxxx必须完全一致

• 由于端口号不同,浏览器认为这是一个跨域请求。

• 根据同源策略,浏览器阻止了这个AJAX请求,因为它默认不允许跨域请求。


• 解决跨域问题:
1
在响应头中添加`Access-Control-Allow-Origin`字段,设置为`*`来允许所有前端来源的的访问。

2
在控制器类前
@CrossOrigin


9.1拦截器 需要额外的配置类,在配置类中配置 配置拦截哪些路径的访问。配置类实现implements WebMvcConfigurer接口或者@bean将webmvcconfiguration注册到容器中,拦截器类 实现implements HandlerInterceptor

多个拦截器拦截同意路径执行顺序:preHandle1- 2-3>目标控制器中的方法>postHandle3-2-1>afterCompletion3.2.1

控制器中的目标方法如果没有正常执行,postHandle也不执行,但是只要对应的preHandle及其前面的prehandle放行,对应的afterCompletion就执行,不管postHandle

任何preHandle不放行,目标控制类方法就不执行,所有的postHandle都不执行,但是对应放行的preHandle对应的afterCompletion会执行


10.1  声明式异常处理(出现异常时自动找到绑定的异常处理方法,实现不应每次都写(try catch)

怎么做:
局部:只能处理本controller类的异常
在控制器类中添加一个方法,专门用来调用R.error()去返回错误信息,方法前标记@ExceptionHandler(错误类型),该控制器类中的其它方法中一旦出现异常,自动找到写上@ExceptionHandler(错误类型)且能够匹配上异常类型的异常处理方法,并执行,在一场处理方法中R.error()

全局:
类名前:@ControllerAdvice 
//对所有的controller类增强,处理所有controller中的异常
方法名前:
@ExceptionHandler(value = Exception.class)
//声明这个方法匹配什么异常类型

注意!全局异常处理类中一定要声明@ResponseBody将返回的R转化为json,为什么局部的处理不用?因为@RestController集成了@@ResponseBody

本类优先>精确优先@ExceptionHandler(精准的异常类.class)>@ExceptionHandler(Exception.class)


10.2企业中最佳实现1:
后端只写一套完整额的正常的业务逻辑流程,一旦出现异常的情况,直接抛出(异常信息或者异常状态码),中断业务,不进行处理

而前端去感知异常(信息或码),这就要求后端判断出现异常时(if id值为空),return中断时告知给上层中断的原因(是执行完成了还是出现异常了)

解决:不再再用return而使用throw new 自定义异常类(传递给自定义异常类的异常码,信息)
需要自定义一个异常类(异常的POJO类,code,msg)
自定义异常类MyExceptionn

最佳实现2:每次都自己写死异常码后期要修改维护太困难,因此构建一个枚举类型的异常码类(相当于就是一张表k/v)然后每次抛出异常throw new 自定义异常类()传的参数就是从这里取出的异常码。最终通过全局的异常控制类处理这个异常返回给前端json数据

注意!枚举类中不能写@Data,只能用@Getter


11.1  数据校验,验证数据是否合法,是否为空
使用JSR 303框架,使用注解标记哪些数据进行校验,
先导入依赖
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

使用:在需要校验的属性的POJO类中,属性名前标记注解即可:
@NotBlank(message = "用户名不能为空")  //11.1 用户名不能为空
    private int id;
其他的:
@Pattern支持校验正则表达式
@Pattern(regexp = "^[0-9]{11}$",message = "手机号格式不正确")·   


然后:告诉spring框架开启校验,在需要传入POJO类对象的方法参数中声明@Valid,如:
@PostMapping("/employee")
    public String add(@RequestBody @Valid Employee employee)

在@Valid Employee employee后面紧跟一个, BindingResult result
result属性封装的@NotBlank这些注解中的message属性值(信息)

效果:校验不通过,@Valid 标记的方法不会执行

 
11.2 全局校验
将上述所有功能优化到一个GolbalError处理类中@ExceptionHandler标记一个方法处理所有异常,在这个异常处理方法中实现上述的逻辑,业务逻辑中只实现正常的逻辑,而不主动判断,出现异常由异常处理类捕获


12.1  初试分部式?为什么用分部式?
引入:数据校验时:@PostMapping("/employee")
    public String add(@RequestBody @Valid Employee employee)  @Valid开启校验,假如说现在其他的业务逻辑也需要校验,但是POJO只有一个,对POJO中的属性的注解@NotBlank等等都是固定的,但是可能新的业务逻辑中不需要检验是否为空,而是检验其他,这就出现矛盾,只能在@NotBlank中添加分组属性,很麻烦

解决办法:不同的业务逻辑对应一个不同的POJO,你对应的业务需要使用什么POJO属性就在新的POJO里写什么属性,业务方法处理时传入这新写的POJO对象

但是又有新的问题出现了:service,dao层的方法都是些好的,返回值都是原本的POJO类对象啊,真正查询的方法也是原本的POJO对象,只不过是在外面封装了一层方法(controller处理),虽然可以像这层方法传递新的POJO类对象,因此还是要转化为为最开始的POJO对象,有属性值的属性赋新值,没有的则为空,这个操作很繁琐,因此使用
BeanUtils.copyProperties(新的POJO对象,原POJO对象)   //原POJO对象是真正去查询的对象,也是查询方法限制必须是它,


13.1  快速生成接口文档(相当于个用户手册吧):Swagger,Knife4j框架
导入依赖:<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
    <version>4.4.0</version>
</dependency>

创建一个配置文件application.yaml
写入L:# springdoc-openapi项目配置
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    tags-sorter: alpha
    operations-sorter: alpha
  api-docs:
    path: /v3/api-docs
  group-configs:
    - group: 'default'
      paths-to-match: '/**'
      packages-to-scan: com.xiaominfo.knife4j.demo.web     //这一行要替换为Controller包所在的路径,为哪些类生成文档就写那些路径
# knife4j的增强配置,不需要增强可以不配
knife4j:
  enable: true
  setting:
    language: zh_cn


直接启动项目,访问http://localhost:8080/doc.html#/home即可

注意!可能会出现版本不兼容的情况,导致显示不出来接口,更改pom.xml中的springboot版本为3.3.3


13.2: 在controller类前标注@Tag(name = "员工管理")声明这个controller的作用,在文档中显示中文设置的name属性值

在方法名前:@Operation(summary = "分页查询员工")声明该接口的作用,作用同上

在POJO类前定义@Schema(description = "员工表")  声明该POJO类专门用于哪个业务,比如说修改员工(只是明明一下方便区分没有实际效果)

在POJO属性名前@Schema(description = "用户名")  ,标记这个属性名,文档中都会中文显示

@Parameters({@Parameter(name = "id",description = "员工ID"),@Parameter(name = "" ,description = "")})  //13.2文档注解  --方法所需的参数

@Parameters({@Parameter(name = "id",description = "员工ID" ,in = ParameterIn.PATH,required = ture)}) 

name参数名,description中文描述文档中展示,in参数来自于哪里(路径参数/请求参数/),required是否必须  

R中的方法和参数用于返回JSON数据,因此也可以添加注解来描述

14.1小细节  数据库中的日期格式datetime(日期加时间弹出日历自己选择日期)如何序列化反序列化(前端提交的Json与POJO对象的属性转化)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss" ,timezone = "GMT+8")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss" ,timezone = "GMT+8")
    //返回给前端的JSON中格式:2023-03-26 16:50:00
    //前端提交的JSON中格式:2023-03-26 16:50:00也会同样转化
    private Date birthday;

15.1MyBatis半自动框架,需要自己写sql,导入依赖SQL-->MyBatis Framework  和  MySql Driver

在spring配置文件中配置数据库连接的信息

在dao层写接口Interface上标记@Mapper //15.1 告诉mybatis这是一个接口(使用mybayis)

鼠标放在类名上,ALT+回车  --> mybatisx generate XML  ,在resources目录下创建mapper目录存放所有的mybatis配置文件.xml

在spring的配置文件中告诉mybatis的配置文件(若干mapper)的位置

15.2  栽培配置文件中<mapper namespace="org.example.practice06_mybatis_test.dao.EmpMapper">

尝试:<select id="getEmpById" resultType="org.example.practice06_mybatis_test.bean.Emp">  <!-- id  表示对应dao类中的哪个方法 ,result指定返回值类型自动将数据复制为POJO对象-->
        select id,emp_name empName,age,emp_salary empSalary from t_emp where id = #{id}   <!-- #{id}  动态绑定要参与查询的传入的参数,参数从 -->
     </select>

<!-- namespace  表示对应哪个dao类实现其中的查询方法 -->

<!--sql中 #{id}  动态绑定要参与查询的传入的参数,参数从 -->

<!-- select标签中 :id  表示对应dao类中的哪个方法 ,result指定返回值类型自动将数据复制为POJO对象-->

<!-- 针对POJO类中的属性与数据库中属性名不一样的情况,精准查询,emp_name是字段名 empName是属性名 -->

select id,emp_name empName,age,emp_salary empSalary from t_emp where id = #{id}

以后dao包命名为mapper,dao层的接口命名为xxxMapper

注意!标签中的sql不用写""

在spring配置文件中:logging.level.org.example.practice06_mybatis_test.dao.EmpMapper=debug
#作用:在org.example.practice06_mybatis_test.dao.EmpMapper包下的所有sql执行时会打印日志


15.3  在insert标签中(插入)useGeneratedKeys="true"表示使用自增的值(主键自增)
keyProperty="id"  表示将这个自增的值赋值给pojo对象中的”id“属性值

然后在mapper(dao)层方法调用后:empMapper.addEmp(emp);
Integer id = emp.getId();  就可以获取返回的这次添加自增的id的值


15.4查询所有,返回多个POJO对象
resultType返回值还是POJO对象(并不更改为list)


针对POJO类的属性与数据库表中的字段名不匹配的情况:
在spring配置文件中:
#开启驼峰命名转化
mybatis.configuration.map-underscore-to-camel-case=true

POJO类中userName,自动转化为表中的user_name


15.5 ${}  和  #{}
${}底层是直接编译,将传入的查询参数(id)直接拼接到sql中(sql中生成占位符),不安全,产生sql注入问题
#{}底层是预编译,安全

场景:需要使用${}:在sql中  表名等想用动态参数,只能用${}来接收参数


//15.6测试查询方法时传单参数
Map和Bean和list作为单参数传入,sql中怎么获取值

15.7多参数查询:在参数前标记@Param("uid") int id起别名,一方面允许多参数,一方面方便在sql中#{别名}捕获  

15.8  多种返回值map,list等返回值
注意的是:
返回值为list时,sql标签中的resulttype属性的值是list中的元素的类型(即Emp类)

返回值为Map时@MapKey("id")  //告知使用每个查询出来的Emp对象的ID属性作为Map的键(Integer)
    Map<Integer, Emp> getAllMap();  //15.8测试获取Map返回值,返回的Map中id为键,查出的所有Emp为值
        //一个细节,返回的Map中的查询出的每一个Emp对象并不是Emp类而是属于map类的k/v中的值,因此无法取出Emp中的属性值
}

因此,如果以后返回值是list或map,在sql标签的resulttype属性都设置为数组中元素类型(即Emp)可以方便去除其中的属性


15.9 :针对POJO类的属性与数据库表中的字段名不匹配的情况:
方法一:在spring配置文件中:
#开启驼峰命名转化
mybatis.configuration.map-underscore-to-camel-case=true

POJO类中userName,自动转化为表中的user_name

方法二:在写sql语句时为列起别名:
 select id,emp_name empName,age,emp_salary empSalary from t_emp where id = #{id}


emp_name是数据库中的字段名,empName是POJO类的属性名

方法三:自定义结果集ResultMap
示例在ReturnMapper.xml中

15.10  关联查询(外键)
多对多关系需要一个外键表


//15.10.1一对一:一个订单对应一个客户,
//根据订单id查询订单信息和订单对应的客户信息
默认情况下:查询出的数据根据resultType指定的返回值类型取匹配属性名并赋值,最终组成一个POJO对象,现在查询出的结果包含两个POJO对象(customer和order)仅仅指定返回值类型为Order是不够的,需要使用自定义结果集来告诉他查出的customer类的信息要怎么封装为customer的POJO对象

见OrderMapper.xml


注意点1!:column属性的值对应的是数据库中的字段名,但是如果在sql中为列名起了别名:就必须填写别名


//15.10.2一对多:一个客户对应多个订单
见CustomerMapper.xml


//15.10.3   分步查询  
原始方法:根据第一次查询出的结果在查询第二次,写两个sql,手动调用两次,第二次的参数为第一册的输出值

支持自动连续调用两次
见FenBuChaXun.java


//15.10.3   究极复杂查询
按照id查询订单,下单的用户,并根据用户查询出该用户的所有订单

需要注意一个点!:最后一次查询的select标签中千万不能放redultMap  避免调用前几次的自定义结果集(包含自动分部查询的)形成死循环,最后一次查询一定resultType只指定最终结果


15.11  延时加载
在在spring的配置文件application.properties中
# 配置MyBatis
mybatis.configuration.lazy-loading-enabled=true
mybatis.configuration.aggressive-lazy-loading=false

效果是:在调用mapper的方法查询出数据后封装为对象
Customer customer = customerMapper.selectCustomerById(1L);

只有在需要某个属性时才会查询:
sout customer.getOrders()  //才会查询,并只显示哪个属性
如果直接打印customer,还是会完整查询并输出


/15.12动态sql:场景,根据前端传来的参数要判断是否为空,是否合法,以此来决定是否将接收的参数插入到sql中参与查询

/15.12.1  <if test="name!= null and name!= ''">if判断通过才会插入包裹的sql片段
    注意一点!不插入的某个片段中出现and where,本来后面还有的sql因为没通过所以没插入,导致出现:select * from t_emp where  或者select * from t_em where  name = 。。 an d  没了
/15.12.2这时候就需要where标签:动态sql之where标签  如果if没有通过,自动删除sql中多余的where and 等  而且还能起到sql语句中where的作用
15.12.3  update更新时也会出现上面的问题,使用set标签效果等同where标签,if不通过时自动删掉多余的and或, 与if合用实现更新时只更新传入参数的列,没传入参数的就不更新
15.12.4  trim标签
<!-- trim标签的属性   prefix = "where"  前缀 suffix后缀 作用:如果trim标签包裹的部分有内容,则添加一个前缀插入到sql语句中,如果没有则不添加  
    prefixOverrides="and | or"  前缀覆盖:在添加prefix前缀之前先判断如果包裹体中的sql语句是以and或者or开头,则使其失效,再添加前缀   suffixOverrides  后缀覆盖 -->
15.12.5  choose标签   相当于java中的switch
15.12.6 foreach标签  作用:用于遍历集合,实现in查询-->
    <!--原始写法 直接获取数组中的[0][1][2]  取值:<select * from  where id IN ([0][1][2])这样的问题是无法确定列表中这一位是否有值-->

开启批量执行多个完整的sql  并且以;隔开:
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis-example?allowMultiQueries=true

# 配置数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/your_database
spring.datasource.username=your_username
spring.datasource.password=your_password

# 配置MyBatis
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.use-generated-keys=true

# 开启批量操作
mybatis.configuration.batch-enabled=true

# 其他MyBatis配置
mybatis.mapper-locations=classpath:mapper/*.xml

# 配置事务管理器
spring.jdbc.template.max-rows=1000

# 开启事务注解驱动
spring.transaction.annotation-driven=true


15.13  开启事务
在启动文件Practice06MybatisTestApplication.java中

@EnableTransactionManagement开启事务功能
在需要事务管理的方法前@Transactional

15.14 sql片段  作用:将重复的sql提取出来,使用时用include引用即可  提高代码的重用性
    使用时:  <include refid="updateEmpByIf"  对应sql标签的标识符引入即可

15.15   字符转义,不要将特定字符识别为sql语句或标签,只识别为普通的符号
原始的<  >'  "  &要写成&amp &it等等,用到查询一下

15.16  mybatis缓存机制,
一级缓存:初次查询后的数据会临时缓存的奥本地,下一次需要查询时直接从缓存中取出,节省时间开支,事务结束时会清空缓存
默认开启事务级别缓存:在同一个@Transactional标记的方法中若存在两次查询(且都有相同结果)则默认只查询一次,第二次从缓存中直接获取
会失效的情况:两次查询的结果不一致;两次查询之间又进行了一次增删改操作,都会失效

二级缓存:事务结束时,将查询结果复制到二级缓存中然后清空一级缓存,下一次查询前,先查看二级缓存,,再看一级缓存,最后没有缓存才会开始二次查询,需要二级缓存的类中标识<cache/>即可生效


15.17  mybatis的拦截器
作用是拦截sql的执行,不常用


15.18  分页查询!
PageHelper插件

逻辑:sql中“limit”限制查询假设每一页有十条数据:则第一页的批量查询:limit 0,10 (从第零行开始查出10行数据(0-9)) 第二页:  limit 10  10(从第10行开始,查出10行(10-19))  第三页:  limit 20 10(第20行开始查出10行(20-29))

使用:见官方文档:https://pagehelper.github.io/docs/howtouse/#1-%E5%BC%95%E5%85%A5%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6

需要新建配置类,@Configuration加入容器
在配置类中:PageInterceptor pageInterceptor = new PageInterceptor();创建一个分页插件的对象  需要将这个分页插件对象注入到容器中@Bean

封装到service中后,在执行查询方法前使用:
PageHelper.startPage(1, 5);  就可将结果只展示5行   //只需要在开始查询前PageHelper.startPage(1, 5);  起始页码,每页大小


15.19  分页查询前端的逻辑
前端需要总页码,总记录数以渲染页码按钮
当前的页码,查询到的本页数据
后端需要收到页码按钮的页码,返回数据
List<Emp> allemps = empService.getEmps();
        return new  PageInfo<>(allemps); 
//将查询结果集按照PageInfo格式返回

15.20
pageInterceptor分页插件对象有很多可用属性
15.20配置分页插件对象PageInterceptor的属性 注意的一点是他的属性本身被封装为一个对象Properties对象,需额外创建
        Properties properties = new Properties();
        properties.setProperty("reasonable", "true");  //15.21配置分页插件对象PageInterceptor的属性
        // 这个属性是用来配置是否支持合理化查询的    合理化查询是指当传入的页码值超过最大页码值时,会自动查询最后一页的数据  


15.21  逆向生成

快捷链接数据库,右侧边栏点击数据库 ,点击+,点击datasource ,点击mysql,配置表名和用户名密码,测试连接,刷新
选中表 右键mybatisx generation
moudule path :选择需要生成的项目
base package:选择项目下的基础包名:org.example.practice01或com...
relative package:每个表对应的Bean类放在哪个包下
next后,在template中选中default-all还可以生成对应的Mapper(dao)和Mapper的配置文件(写sql的)
生成时不会添加@Mapper注解,解决方法:在配置类中(这里指的是标记@Configuration的类)
@MapperScan("org.example.practice01.mapper")   //扫描mapper包,为所有mapper生成@Mapper注解


我发现mybatisx插件在使用逆向生成功能时,在生成的类中package org.example.practice01.bean; 包名会错误的生成为package org/example/practice01/bean; 原因是生成配置时base package:要写成这种点分割的形式:org.example.practice01 才会整成的生成package org.example.practice01.bean;


16  springboot入门
springboot说白了就是使创建项目变得简单,提供可是换选择窗口,自己选择想要的依赖就能自动加入,整合tomcat,提供自定义的场景starter,选择场景快速创建

传统的web开发,将写好的项目打包为war包放在tomcat的webapps目录下
快速打包:右侧栏maven--选中想打包的项目--Lifecycle选项--选择clean和package清空target并打包,直接上传到服务器或者直接cmd执行java命令就行

快捷修改运行时的配置  任何配置修改都可以在启动时修改:如修改端口号,只需:
java -jar 包名  --server.port=9999


16.1场景启动器:在pom.xml中:
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

其中:spring-boot-starter-web这就是web场景

16.2  依赖管理,父版本不管理的所有依赖才需要指定版本


16.3  属性绑定:
将容器中的组件(Bean中的)属性值绑定为配置文件application.properties中设置的值

在Bean中:
@ConfigurationProperties(prefix = "dog")  // 读取配置文件中以dog开头的属性,将其值封装到当前类中对应的属性

配置文件中:
dog.age= 3
dog.name= 旺财

打印一个Bean的对象就可看到注入的值


关于在配置文件中的中文被识别为中文乱码:
setting--Editor---File Encodings --Default encoding for properties file :修改为utf-8 

在启动类中:
@EnableConfigurationProperties(DogProperties.class)
作用1:如果DogProperties这个Bean类没有添加到容器中(没写@Conponent)则将其注入到容器中
作用2:将配置文件中设置的属性值注入到这个Bean的对象中

16.4  新式配置文件application.yaml  以yaml结尾的文件:换行缩进表示层级关系;#注释;日期格式以/隔开:如果两个配置文件同时配置同一个属性,前者优先
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.1.100:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
    username: root
    password: root
jpa:
    show-sql: true
    hibernate:

可以配置复杂的值(数组,列表,集合,对象)作为Bean中的属性值绑定到Bean中:
person类中有一个对象child作为其属性:

person:
  name: zhangsan
  age: 12
  child:
    name: lisi
    age: 12

child类中有一个数组作为属性
  child:
    name: lisi
    age: 12
    text: ["1","2","3"]    #字符串需要加"",而在配置文件中不需要加""
    或:text:
        -1
        -2


数组里面是若干个对象:
    text:
      - name: 王八蛋
        age: 12
      - name: 王二蛋
        age: 12
      - name: 王大蛋
        age: 12
或:
    text:
      - name: 王八蛋
        age: 12
      - name: 王二蛋
        age: 12
      - {name: 王二蛋, age: 12}


map中一个string作为键,一个对象作为值:
    cats:
      blucat: 
        name: 王二蛋
        age: 12
      redcat:
        name: 王二蛋
        age: 12        或:
      blackcat: {name: 王蛋, age: 12}

16.5好玩:
在配置文件中
spring.banner.location=classpath:banner.txt  #在本项目路径下放一个banner.txt文件,其中放置自己想写的LOGO
!!!!classpath匹配的是当前项目中的src/main/resources路径

16.6  :日志以后用日志来代替sout输出的调试信息
在测试类中:
Logger logger = LoggerFactory.getLogger(Loggertest1.class);

获取一个logger对象,参数是想要生成日志的目标类。
logger对象去掉用各种方法输出日志信息:
.debug   .info   ......

渐变获取logger对象:
在类名前@Slf4j  会自动获取一个log对象


16.7
//日志级别从低到高:分别是:项目的的日志级别越高,显示的日志越少()
        //ALL <TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF

在配置文件application.properties中:
logging.level.root=info或debug或其它
.root是全局
.主类包.包名=  就只设定这个包中的日志级别
如:org.example.practice07springboot.controller

默认是info级别

分组:
logging.group.biz= org.example.practice07springboot.Logger , org.example.practice07springboot.Logger2   //若干个包或类分为一组统一管理级别

logging.group.biz.level= debug


16.8  指定一个文件保存日志信息:
logging.file.name=boot.log  //文件名可以没有这个文件,会自动创建,保存在项目名所在目录下
logging.file.path=D://boot.log  //指定路径和文件名

16.9  
日常日志文件归档和日志文件切割(保持文件大小不会太大)

!!!!一个有意思的点.info()日志输出方法中
info("日志的字符串信息....{}  ,i++")
字符串中{},可以将后边i++这样的动态变换的字符串自动填充到{}中  

设置归档:超过最大文件大小就会切割并压缩,换一个新的文件开始记录
#日志文件路径及命名方式
logging.logback.rollingpolicy.file-name-pattern=boot-%d{yyyy-MM-dd}.log
#是否在启动时删除旧日志
logging.logback.rollingpolicy.clean-history-on-start=true
#最大文件大小
logging.logback.rollingpolicy.max-file-size=10MB
#总文件大小
logging.logback.rollingpolicy.total-size-cap=100MB

16.10
环境隔离:不同环境下不同的组件不同的配置:
配置文件application.properties支持命名区分application 环境标识xxx.properties来区分不同环境,每种环境添加自己的配置
在各种组件(Bean)前@Profile("环境标识")
激活环境:在主配置文件application.properties中:
spring.properties.active=环境标识

//无论激活那个环境都需要激活的环境标识(标记的bean类或者配置文件)一般数据库连接等配置文件以这种方式激活
spring.properties.include=环境标识

16.11
外部配置机制,在项目打包为jar后,欲修改application.properties或application.环境标识.properties,不需要直接修改jar包中的配置文件再重新打包,而是直接在jaar包外部放置同名的文件application.properties,对这个修改后运行时之际影响到原jar包:

原则:外部优先(config包中优先级更高),带环境标识的优先,同时有标识和外部的情况,带环境标识的优先

16.12 参院测试的一些注解:
@BeforeEach和@AfterEach
被标记的方法会在所有测试方法@Test执行前执行,执行后执行,前者一般用于初始化所有测试方法需要在使用的资源对象环境等等,后者关闭连接,资源,清理缓存

断言机制:test测试通过的机制就是方法不报错即可,但是单元测试时真正判断业务逻辑是否正确应该判断是否输出想要的结果
@Test
    public void test2() {
        String fanhuizhi = loggertest1.test2();
        //测试断言机制
        Assertions.assertEquals("1234",fanhuizhi,"断言失败");
//  若参数二的值等于参数1的值,则成功通过测试
若不等于,则报错输出自定义的message信息(参数三)

其他:
.assertNotEquals("123",fanhuizhi,"断言失败");
.assertThrows(异常类型) ->{ 可能产生异常的方法  };   即使有错误无法运行,只要抛出异常,就通过测试


16.13
可观测性指标:
pom.xml中搜索依赖:springboot  actuator
在配置文件中:
management.endpoints.web.exposure.include=*   暴露出所有指标

运行后:访问http://localhost:8080/actuator即可看到所有指标

16.14  监听springbbot的运行生命周期

一个类实现
implements SpringApplicationRunListener接口:

几种生命周期函数
starting:正在启动主要是启动IOC容器
started  IOC容器加载完成,因此这两个都无法输出
ready
failed
contextLoaded
contextPrepared

在src/main/resources/META-INF/spring.factories中
org.springframework.boot.SpringApplicationRunListener = org.example.practice07springboot.Mylistener   //指定写生命周期方法的类


16.14  事件驱动开发:
controller监听请求,请求接受后,发出一个事件,然后各种服务类(提供业务逻辑)监听事件,监听到和自己有关的事件发生时产生效果,
而不是以前:controller接收后,硬编码自己写上各种服务方法的调用

    1首先定义事件类event,定义触发这个事件时所需要的参数和一个有参构造方法this.参数赋值
    2在controller层中注入一个ApplicationEventPublisher对象(事件发生器)
    3在controller的方法中,发布事件:
new 自定义事件类(所需参数)
    事件发生器对象.publishEvent(new的自定义事件类对象)
    4 在service里或者实际产生操作的类中定义一个方法@EventListener(自定义事件类.class),这个方法监听到事件会调用,在其内部调用本类中真正产生业务的方法即可

限制:当前的事件只能在本项目中发送无法跨项目(本地消息模式)

分布式系统:需要一个独立的消息(事件)服务器,专门用来发消息

异步:在需要异步的方法前@Async,在类名前:@EnableAsync  开启异步功能,每个方法会基于不同的线程调用(快)


16.15自定义starter(相当于写好的jar)
自定义的starter不需要启动类
需要引用时,在pom.xml中添加这个starter名的depency
注意抽取出来的代码复制到starter(新项目中时)检查包名
主程序(启动类)只会扫描本项目下的所有组件注解,而自定义的starter中的注解就无法识别
自定义的starter需要一个配置类(@Configuration)在这个配置类中@Bean手动将组件插入容器
在欲使用的项目的启动类中@Import导入这个配置类从而将自定义starter的组件也注入新项目的容器

升级,spring项目启动时扫描resource包下的META-INF中的


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值