Spring小细节
引言: emmm,最近就是想着把spring容器吃透一些,然后事务繁忙所以主要就看了看b站的ssm视频,没有很仔细的敲代码(因为学过一些),然后里边的AOP动态代理可以在b站看看视频一定要学会,这个由于时间问题没记多少东西,但其实AOP主要就用到了注解加切面这俩个类(如果对数据库操作切面还要包含对应的业务层和持久层等),AOP很有用一定要搞懂,希望以后大家都能找到一份好工作。
Spring
概念
1.狭义上spring框架指的是Spring Framework(Spring Framework 整合了 Spring MVC)而广义上spring体系才是全家桶。
2.有功能的类就叫做组件(万物皆组件),比如service方法,而如果是单纯保存数据的类不叫做组件,比如pojo类。
3.容器管理组件就不用new了,IOC控制反转思想就是通过DI依赖注入(setter、构造器等方式)来实现,自动注入加生命周期。
4.注意容器打印的对象如果定义返回值就直接返回对象,否则就是返回地址(全限定类名(包名 + 类名)+@哈希码)因为没有重写toString,所以会继承 Object类的默认实现。
5.容器具有单实例特性,每个对象只会创建一次。
6.spring容器一般会用config包下的配置类来专门存放,类上必须加@Configuration,而bean配置文件没以前流行,这样比较好分类管理。并且如果类上没有@Configuration光方法有bean也不会生效,类必须注册进bean才可以。
7.配置类也是一个容器组件,比如personConfig(默认是类名小写),不过里边的方法我们都需要手动@Bean(只能加在方法上)注册。
8.像@Controller、@Service等MVC分层注解会自动扫描并创建实例,所以不需要手写@Bean了,而且写了的话理论可以,但不规范。
9.注意分层注解能起作用的前提是: 这些组件必须在主程序所在的包及其子包下,否则@ComponentScan(basePackages = "com.rjgc.spring") //组件批量扫描;只扫利用Spring相关注解注册到容器中的组件,类上不加@Component等注解就不扫。不过规范编程即可避免问题,而且即使这样写,包看着也难受。
10.要想注册容器,要么用mvc注解,要么就用@Configuration,然后只要是bean容器哪个类上都可以标@Import等注解,不过可以在config一个类下专门处理,这样让springApplication主程序更加清爽。
11.将第三方类注册bean,要么@Bean将其返回值写进去并且return,要么@Import(xxx.class),注意如果有的第三方类名重复,那么我们就@Import(全限定类名.class)指定注册。
12.容器的所有组件我们都可以称之为bean,将bean注册进去之后我们就可以注入不用new对象了。
Bean
注册
modules就是聚合,记得刷新,不过子项目不继承父项目的话,父项目没有控制权
public static void main(String[] args) { //1、跑起一个Spring的应用; ApplicationContext:Spring应用上下文对象; IoC容器 ConfigurableApplicationContext ioc = SpringApplication.run(主类.class, args); System.out.println("====================ioc容器创建完成===================="); //4、获取容器中的组件对象;精确获取某个组件 // 组件的四大特性:(名字、类型)、对象、作用域 根据名字或类型就可以拿到对象,进而进行操作 // 组件名字必须全局唯一 (老版本好像一定只会放一个最先声明的bean(可能按首字母或者注册顺序),这样以后的bean就不能注册了) // 新版本好像会报错 BeanDefinitionStoreException 所以方法重载bean名称不同理论可以,但是这样就没意义了 //小结: //从容器中获取组件, // 1)、组件不存在,抛异常:NoSuchBeanDefinitionException // 2)、组件不唯一, // 按类型只要一个(如果多个组件是同类型的):抛异常:NoUniqueBeanDefinitionException // 按名字只要一个: 可以精确获取到指定对象 // 同类型有多个:返回所有组件的List集合 // 同名称不会有多个 // 3)、组件唯一存在,正确返回。 //4.1、按照组件的名字获取对象 Person zhangsan = (Person) ioc.getBean("zhangsan"); System.out.println("对象 = " + zhangsan); //4.2、按照组件类型获取对象 (按类型只有一个bean才不会冲突) //Person bean = ioc.getBean(Person.class); //System.out.println("bean = " + bean); //4.3、按照组件类型获取这种类型的所有对象 Map<String, Person> type = ioc.getBeansOfType(Person.class); System.out.println("type = " + type); //4.4、按照类型+名字 (这样对比直接获取名称好处就是不用强制类型转换) Person bean = ioc.getBean("zhangsan", Person.class); System.out.println("bean = " + bean); } @Bean("可以指定名称") // 不指定名称默认就是方法名 public Person zhangsan() {Person person = new Person();person.setName("张三");person.setAge(20);person.setGender("男");return person; } /** *前提必须注册到容器中 *打印结果: *对象 = Person(name=张三, age=20, gender=男) 按照名称拿取,需要强转,默认拿取的是Object *bean = Person(name=张三, age=20, gender=男) 按照类型拿取容器中对象的好处是不用强转 *type = {zhangsan=Person(name=张三, age=20, gender=男), lisi=Person(name=李四, age=20, gender=男)} 直接获取多个 */ 当然这个格式也是@Data注解重写toString后的效果
public class Spring01IocApplication{/*** Spring为我们提供了快速的MVC分层注解,有了这些注解就不用每个方法都@Bean了* @Configuration + @Bean:用于定义第三方组件或需要自定义创建逻辑的组件,必须手动编写@Bean方法。Spring特有注解* 1、@Controller 控制器* 2、@Service 服务层 (这些底层都封装了@Component)* 3、@Repository(传统的JDBC,标注在普通类上) @Mapper(MyBatis框架特有,并且必须是接口) 持久层* 4、@Component Spring核心组件(其实都标上这个就可以处理了,只不过上述三个给我们看会意思更明确一些)* @param args*/public static void main(String[] args) {ConfigurableApplicationContext ioc = SpringApplication.run(Spring01IocApplication.class, args); System.out.println("========"); UserController bean = ioc.getBean(UserController.class);System.out.println("bean = " + bean); UserService bean1 = ioc.getBean(UserService.class);System.out.println("bean1 = " + bean1);} }
优雅写法,虽然注解标注灵活,但是这样会更清爽
@Import({CoreConstants.class,Car.class}) 也可以是数组形式,而且不仅第三方照样也可以导入自己写得类 @Configuration @ComponentScan(basePackages = "com.atguigu.spring") public class AppConfig { }
scope: 补充小知识,不过基本用不到
/*** 不过无论是否单实例,加上@Bean或其他mvc注解都是被立即注册进容器的,只不过对象创建时机会不同* @Scope 调整组件的作用域:* 1、@Scope("prototype"):非单实例:* 容器启动的时候不会创建非单实例组件的对象。* 什么时候获取,什么时候创建 (并且非单实例对象不会复用) (懒加载)* 2、@Scope("singleton"):单实例: 默认值* 容器启动的时候会创建单实例组件的对象。* 容器启动完成之前就会创建好 (饿汉式) (只有单实例可以加上@Lazy注解,这样可以变成懒加载,主要是缓解内存)* 3、@Scope("request"):同一个请求单实例* 4、@Scope("session"):同一次会话单实例*/
// FactoryBean在容器中放的组件的类型,是接口中泛型指定的类型,组件的名字是 工厂自己的名字
// 这个是工厂bean.如果创建对象很复杂时才用到。
@Component // 也要注册容器 public class BYDFactory implements FactoryBean<Car> {// 我们实现三个方法即可,分别是制作谁、制造的类型(这个主要用于多态)、是否单例。 }
//拿到环境变量 ConfigurableEnvironment environment = ioc.getEnvironment(); String property = environment.getProperty("OS"); System.out.println("property = " + property);
条件注解(可以满足条件才注册到容器)
@Conditional(MacCondition.class) // 标注在普通类或方法上没有效果,因为本身不负责注册bean // 可以标在容器组件类或Bean方法上,必须是容器才可以 这个注解可以点进去看看,内容可以是数组 ------------- public class MacCondition implements Condition { // 实现的这个接口也是规范@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {// 只要是上下文就可以拿到,可能是不同的实现方式Environment environment = context.getEnvironment();String property = environment.getProperty("OS");return property.contains("mac");} } 而且该类只是一个判断条件的工具,所以不需要注册到bean
注入
前提每个类都要在bean中注册了,设计模式:而且我们一般注入接口,这样可以实现多态,依赖倒置,因为接口不咋变,而实现类可能经常改变。
@Autowired userController = UserController(abc(方法名)=com.atguigu.spring.ioc.service.UserService@68e62ca4)
personList=[Person(name=比尔盖茨, age=20, gender=男), Person(name=张三2, age=20, gender=男), Person(name=李四, age=20, gender=男)] 这个是集合,区分上边的map是每个都加上前边的方法名了,而这个只需加一个参数名 注意当一个类中的参数依赖注入时,获取到的ioc里面默认参数是map格式
/*** 自动装配流程(先按照类型,再按照名称)* 1、按照类型,找到这个组件;* 1.0、只有且找到一个,直接注入,名字无所谓* 1.1、如果找到多个,再按照名称去找;变量名就是名字(新版)。* 1.1.1、如果找到: 直接注入。* 1.1.2、如果找不到,报错* 这个也是bean(类是一个bean组件,比如@Controller)中依赖注入bean(找我们所有配置好的bean)*/@Autowired //自动装配; 原理:Spring 调用 容器.getBean UserService abc; // 这个类型下只有一个所以随便写@Autowired // 精确名称 Person bill;@Autowired //把这个类型的所有组件都拿来 List<Person> personList; // 只能带类型@Autowired Map<String,Person> personMap; // 这样显示的结果会带上注入的bean的方法名称@Autowired ApplicationContext applicationContext; // 也可以找到
@Qualifier("bill") //精确指定:如果容器中这样的组件存在多个,且有默认组件。我们可以使用 @Qualifier 切换别的组件。 @Autowired Person atom; // 其实这个精确指定名称通过自定义方法名也可以实现,不过如果@Primary标注之后就需要通过这个注解来切换bean了
//面试题:@Resource 和 @Autowired 区别? //1、@Autowired 和 @Resource 都是做bean的注入用的,都可以放在属性上,只有Spring家族可以使用 //2、@Resource 具有更强的通用性,javaEE也可以用@Resource // 没找到就报错 UserDao userDao;@Autowired(required = false) // 这个是特有的,即使没找到为空即可这样不报错,不过还是建议用这个就足以了 // 注意这个是没注册到bean不会报错,如果注入后找不到仍然会报错,找不到的原因同上 Dog dog;
@Autowired // setter注入也是另一种方式 public void setDog(@Qualifier("dog02") Dog dog) {System.out.println("setDog..." + dog);this.haha = dog; }
感知接口
@Service // 前提也是注册到bean然后就会自动调用setter方法 public class HahaService implements EnvironmentAware {private Environment environment;@Override // 内部已经写好,我们实现这个即可,所以不需要@Autowired,自己写需要public void setEnvironment(Environment environment) {this.environment = environment;}public String getOsType(){return environment.getProperty("OS");} }
非常强大的@Value注解,所以pojo类如果别的类不用的话,也可以加上@Component来注册到bean并赋值
// 当然前提也要注册到bean @Component会扫描到 /*** 1、@Value("字面值"): 直接赋值* 2、@Value("${key}"): 动态从配置文件中取出某一项的值。 $* 3、@Value("#{SpEL}"): Spring Expression Language: Spring 表达式语言* SpEL就代表可以写表达式 #*/@Value("旺财") private String name; @Value("${dog.age : 默认值}") // 读取配置文件yml的信息,如果没有设置为默认值就不会报错了 private Integer age;@Value("#{10*20}") private String color;@Value("#{T(java.util.UUID).randomUUID().toString()}") // 如果是静态方法必须加T() private String id;@Value("#{'Hello World!'.substring(0, 5)}") // 非静态方法 private String msg;@Value("#{new String('haha').toUpperCase()}") private String flag;@Value("#{new int[] {1, 2, 3}}") // 注意如果我们new自己的类则该语法需要带上全类名s private int[] hahaha;
属性来源
//说明属性来源: 把指定的文件导入容器中,供我们取值使用 @PropertySource("classpath:xxx/cat.properties") // 注意classpath表示的是从类路径开始即当前项目的resources目录 // 虽然注解可以随便标,但还是建议遵循单一职责,不要防御性编程 @Data @Component public class Cat {@Value("${cat.name:Tom}") // : 后面是取不到的时候的默认值;private String name;@Value("${cat.age:20}")private int age; }
注意
1.classpath:cat.properties;从自己的项目类路径下找
2.classpath*:Log4j-charsets.properties;从所有包的类路径下找(比如第三方依赖),如果jar包下一点开刚好看到就这样写,否则
classpath*:xxx/xxx/xxx.properties即可
3.ResourceUtils也可以获得资源文件,比如classpath: jar: file:等,也可以拿取到网上的uri图片地址
当然我们依赖注入时不仅可以注入类,拿到对象调用方法,还可以直接注入方法
//1、定义环境标识:自定义【dev、test、prod】; 默认【default】 //2、激活环境标识: // 明确告诉Spring当前处于什么环境。 // 你要不说是啥环境,就是 default 环境//利用条件注解,只在某种环境下激活一个组件。 @Profile({"dev","default"}) // @Profile("环境标识")。当这个环境被激活的时候,才会加入如下组件。 @Bean // 写成数组格式,这样满足一个即可,不会报错 public MyDataSource dev(){MyDataSource myDataSource = new MyDataSource();myDataSource.setUrl("jdbc:mysql://localhost:3306/dev");myDataSource.setUsername("dev_user");myDataSource.setPassword("dev_pwd");return myDataSource; }
@Profile
spring.profiles.active=test
这个就是在yml中写出来即可激活环境了,刚好对应我们的注解@Profile
而且这种也不想@Value那样写$,直接写即可
post后置,pre之前,最复杂并且功能最全的是BeanPostProcessor,这个能改对象,真正具有拦截功能,而其他只能感知不能修改(其他感知的是void类型,而这个可以有返回值)
AOP
自定义日志
/**
日志:
1、硬编码: 不推荐; 耦合:(通用逻辑 + 专用逻辑)希望不要耦合; 耦合太多就是维护地狱,比如方法中sout
2、静态代理:
定义:定义一个代理对象,包装这个组件。以后业务的执行,从代理开始,不直接调用组件;
特点:定义期间(编码期间就确定好了)就指定好了互相代理关系
实际上就是一个代理类也实现了接口,然后构造对象时传递参数为实际的类,实现方法调用实际类的方法,从而达到代理效果
缺点:需要些很多类来代理不同对象
3、动态代理:
运行期间才确定好了代理关系(目标对象在执行期间被拦截,插入指定逻辑)
注意目标对象必须有接口才可以
4、AOP:
更简洁,优化了对象必须有接口才可以动态代理
*/
数组不能直接打印,所以Arrays.asList(数组)即可,当然Arrays.toString更牛,即使参数为null也可以,而前面那个会报空指针异常
mapper.xml小细节
非常标准的写法: 主要是防止有单独的where或and出现,不过其实<where>标签和<set>已经解决了,所以下方主要是手动处理时的原理。
<!-- prefix:前缀 ;如果标签体中有东西,就给它们拼一个前缀 suffix:后缀 prefixOverrides:前缀覆盖; 标签体中最终生成的字符串,如果以指定前缀开始,就覆盖成空串 suffixOverrides:后缀覆盖 --> <select id="queryEmpByNameAndSalary" resultType="com.atguigu.mybatis.bean.Emp">select * from t_emp<trim prefix="where" prefixOverrides="and || or"><if test="name != null">emp_name= #{name}</if><if test="salary != null">and emp_salary = #{salary}</if></trim> </select>
<update id="updateEmp">update t_emp<trim prefix="set" suffixOverrides=","><if test="empName != null">emp_name = #{empName},</if><if test="empSalary != null">emp_salary = #{empSalary},</if><if test="age != null">age = #{age}</if></trim>where id = #{id} </update>
上述forEach在加个不为空的判断,避免传递的ids为null发生报错,元素之间分割符肯定不会出现末尾单个逗号情况
<!批量新增--> <insert id="addEmps">insert into t_emp(emp_name,age,emp_salary)values<foreach collection="emps" item="emp" separator=",">(#{emp.empName},#{emp.age},#{emp.empSalary})</foreach> </insert> <!批量修改 这个比单纯for循环然后每次都更新一条效率高,缺点就是不能用事务管理,这个是一个长sql管理很多短sql--> <update id="updateBatchEmp"><foreach collection="emps" item="e" separator=";">update t_emp<set><if test="e.empName != null">emp_name = #{e.empName},</if><if test="e.empSalary != null">emp_salary = #{e.empSalary},</if><if test="e.age != null">age = #{e.age}</if></set>where id=#{e.id}</foreach> </update>
# allowMultiQueries 允许一次发送多个sql,并用分号隔开 spring.datasource.url=jdbc:mysql://localhost:3306/mybatis-example?allowMultiQueries=true # xml中必须加上这个才允许用分号隔开
@SpringBootTest // 只是影响速度,但是不会启动我们的主程序 public class HttpClientTest {/*** 测试通过HttpClient发送GET请求* client是客户端的意思*/@Testpublic void testGet() throws Exception{// 1.创建httpclient对象(CloseableHttpClient刚好实现了HttpClient这个接口)CloseableHttpClient httpClient = HttpClients.createDefault();// 2.创建请求对象(当然必须启动才能访问到) 客户端肯定是向我们服务端发送请求HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");// 3.发送请求并接受响应结果CloseableHttpResponse response = httpClient.execute(httpGet);// 验证: 获取响应状态码以及响应数据System.out.println("服务端返回的状态码为: " + response.getStatusLine().getStatusCode());HttpEntity entity = response.getEntity(); // 也是他自带的封装,需要解析String result = EntityUtils.toString(entity);System.out.println("服务端返回的数据为: " + result);// 关闭资源response.close();httpClient.close();}/*** 测试通过HttpClient发送POST请求*/@Testpublic void testPost() throws Exception{// 获取资源CloseableHttpClient httpClient = HttpClients.createDefault();// 创建请求HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");/*** 最重要的一步是封装传递的参数*/// 因为阿里的fatsJson简单一些就用了JSONObject jsonObject = new JSONObject();jsonObject.put("username", "admin");jsonObject.put("password", "123456");StringEntity stringEntity = new StringEntity(jsonObject.toString()); // HttpEntity的实现类// 还要指定一下编码方式和传递的数据格式stringEntity.setContentEncoding("utf-8");stringEntity.setContentType("application/json");httpPost.setEntity(stringEntity);// 发送请求CloseableHttpResponse response = httpClient.execute(httpPost);System.out.println("相应状态码为: " + response.getStatusLine().getStatusCode());HttpEntity entity = response.getEntity();String result = EntityUtils.toString(entity); // 好看System.out.println("响应数据为: " + result);response.close();httpClient.close();} }
日志使用
日志可以代替System.out,因为其可以更方便而且不用全局修改,可以灵活选择记录到文件或打印控制台
级别
我们可以进行日志级别的调整,比如:
if("1".equals(log)){log.debug("不重要的日志......");// 开始执行业务流程try {//关键点log.info("关键日志........"); // 精细一些//容易出问题点aa.bb();log.warn("调用 aa.bb() 方法后注意可能后续版本废除........"); // 这个在里边写即可}catch (Exception e){log.error("错误日志......"+e.getMessage()); // 发生错误记录下原因// 如果是fatal类型的致命错误程序可能直接崩溃} }
# 如果哪个包、哪个类不说日志级别,就用默认root logging.level.root=debug // 可以调整整个项目级别,这样信息更详细 logging.level.com.rjgc.boot=error // 精细某个包下的级别,只打印error即以上 如果不想打印写成off即可
# 设置日志组 logging.group.base=com.rjgc.service,com.rjgc.dao# 按组批量设置日志级别 logging.level.biz=debug # 可以打印更详细的信息 logging.level.web=debug # 这个是系统自动分好组了,可以去了解下
输出到文件
# 当两个都配置时优先以文件名为准 # 表示当前项目所在的根文件夹下生成一个指定名字的日志文件 logging.file.name=boot.log # 表示在生成一个文件夹,里边的名称其实为spring.log #logging.file.path=D://aaa.log
当然我们也可以写logback.xml即可,而且这时以我们写得为准,不在以yml为准了,当然如果我们换框架的话(通过依赖切换日志),xml就没用了。
其实我们xml中主要配置这些即可:
1、配置(日志输出到文件、打印日志级别(可以分组))
2、记录日志(合适的时候选择合适的级别进行日志记录。比如log.error/info/debug)
3、文件归档和滚动切割其实默认都写好了,满足条件自动触发,我们不写也是可以的