Echking's 学习之路 - 学习
type: channel
link: http://eck.ink/category/study/
description: 学习记录
items: SpringBoot2 Web定制错误页面
items: SpringBoot2 默认Web异常处理
items: 『源码分析』ThymeleafViewResolver视图解析
items: SpringBoot2 底层注解
items: SpringBoot自动配置原理（RedisAutoConfiguration）
items: Spring Security笔记
items: IDEA Debug 功能
items: Java进程浅析+案例（初学者适用）
items: Java内部类详解
items: Java字符串拼接的底层原理

SpringBoot2 Web定制错误页面
type: item
date: 2023-04-26T21:52:00+08:00
description: 分析源码在BasicErrorController源码中的errorHtml方法中@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); }会返回一个ModelAndView，其中包含请求信息，响应信息，状态码，和模型数据。在ModelAndView modelAndView = resolveErrorView(request, response, status, model);中的resolveErrorView方法为：protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { for (ErrorViewResolver resolver : this.errorViewResolvers) { ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null; } }它会遍历所有的errorViewResolvers，找到一个匹配的返回。在其中的ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);接口resolveErrorView，其实现在DefaultErrorViewResolver中：@Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; }该方法中的resolve(String.valueOf(status.value()), model);具体方法为：private ModelAndView resolve(String viewName, Map<String, Object> model) { String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); if (provider != null) { return new ModelAndView(errorViewName, model); } return resolveResource(error…

SpringBoot2 默认Web异常处理
type: item
date: 2023-04-26T20:38:00+08:00
description: 默认的异常页浏览器客户端 发生error500报错 (text/html页面)image-20230426184115683404报错 (text/html页面)image-20230426184212061Postman 报错500报错 (Json数据)image-20230426200911775404报错 (Json数据)image-20230426200946513源码org.springframework.boot.autoconfigure -> web -> servlet -> error -> ErrorMvcAutoConfiguration在配置类ErrorMvcAutoConfiguration中，由其注解可知@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })可以在配置文件中通过其前缀来修改该异常配置类的配置。该配置类中的两个Bean可以看出，第一个errorAttributes为错误属性；basicErrorController为控制器，来处理请求。@Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); } @Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), errorViewResolvers.orderedStream().collect(Collectors.toList())); }在BasicErrorController中，可以看到使用了注解@Controller和@RequestMapping@Controller @RequestMapping("${server.error.path:${error.path:/error}}")而在之前的错误信息中，发送的请求是/error所以BasicErrorController就是来处理/error请求的。BasicErrorController中标注了@RequestMapping的方法，第一个（用于浏览器端）：image-20230426202435448第二个（Psotman机器客户端）：image-20230426202518337在ErrorMvcAutoConfiguration的源码中，类WhitelabelErrorViewConfiguration中配置了默认错误页面。protected static class WhitelabelErrorViewConfiguration { ...... }image-20230426203231308当发起错误请求，没有自定义异常来处理，则由BasicErrorController来处理，会检测媒体类型，如果是html页面，没有自定义的错误页面，就会使用默认配置的html页面；若是Postman等机器客户端发起的错误请求，则响应一个Json数据(return new ResponseEntity<>(body,status))。

『源码分析』ThymeleafViewResolver视图解析
type: item
date: 2023-04-25T20:08:00+08:00
description: //跳转至login.html页面 @GetMapping("/login") public String goToLogin(){ return "login" }Thymeleaf是如何将String字符串"login"解析成"classpath:/templates/login.html"的？一、前端控制器DispatcherServlet前端控制器中的doDispacth在处理请求时，渲染页面过程：先通过//DispatcherServlet.class 1043行 mappedhandler = getHandler(processedrequest)查找url是否存在，若存在则返回执行链和适配器，在//DispatcherServlet.class 1067行 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());真正执行Handler然后在//DispatcherServlet.class 1084行 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);渲染视图，而Thymeleaf提供了视图解析器，所以在这里渲染视图的是Thymeleaf的视图解析器。二、ThymeleafAutoConfiguration在ThymeleafAutoConfiguration中，SpringBoot2 就已经为我们默认注册了一个thymeleafViewResolver@Bean @ConditionalOnMissingBean(name = "thymeleafViewResolver") ThymeleafViewResolver thymeleafViewResolver(ThymeleafProperties properties, SpringTemplateEngine templateEngine) { ...... return resolver; }只要没有手动自己添加，则会默认使用该视图解析器。在ThymeleafViewResolver中的createView创建视图方法，会传入一个视图名称，如下代码所示的viewName@Override protected View createView(final String viewName, final Locale locale) throws Exception { ...... }然后通过viewName.startWith()方法截取// Process redirects (HTTP redirects) //此处的REDIRECT_URL_PREFIX的值为： //public static final String REDIRECT_URL_PREFIX = "redirect:"; if (viewName.startsWith(REDIRECT_URL_PREFIX)) { vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName); final String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length(), viewName.length()); final RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, REDIRECT_URL_PREFIX); }然后通过RedirectView构建视图返回。forward前缀同理。如果前缀不符合上述两种，则执行return loadView(viewName, locale);@Override protected View loadView(final String viewName, final Locale locale) throws Exception { ...... }利用loadView最终实例化view = (AbstractThymeleafView) bean…

SpringBoot2 底层注解
type: item
date: 2023-04-23T20:17:00+08:00
description: SpringBoot2 底层注解@Configuration与@Component同作用，但能让其他人更方便地识别为配置类；该注解源码中有一行代码：boolean proxyBeanMethods() default true;1.自定义的配置类，相当于xml配置文件，配置类本身也是组件2.配置类里面使用@Bean标注在方法上给容器注册组件，默认也是单实例的3.属性proxyBeanMethodsFull模式：默认是true 用来监测当前IOC容器中是否有此实例，若有则不会再创建，直接返回此单实例；当前配置类的实例信息：com.example.springboot.config.MyConfig1$$EnhancerBySpringCGLIB$$7c35a8af@aa21042 代理对象Lite模式：指定为false 每一次调用都会创建新的实例；当前配置类的实例信息：com.example.springboot.config.MyConfig1@45673f68使用场景总结：Lite模式，指定proxyBeanMethods值为false，配置类组件之间无依赖关系用Lite模式加速容器启动过程，减少判断配置Full模式，默认指定proxyBeanMethods值为true，类组件之间有依赖关系，方法会被调用得到之前单实例组件注册实例bean至IOC容器中的方法方式一@Configuration + @Bean会将添加了@Bean注解的方法的返回值注册到IOC容器中以方法明作为beanName存在。方式二@Configuration + @Import( {xxx.class})将指定的class对象注册至IOC容器中，以全类名作为beanName存在。全类名：包名.类名方式三@Configuration + @ImportResource( {"classpath:xxx.xml"})将xml配置文件中的实例bean，注册到容器中，翻新SpringMVC项目时常用。@Import( { })将指定的class对象注册至IOC容器中，以全类名作为beanName存在。@Conditional一般称为条件装配。分为：@ConditionalONXxx 表示一定具备需要提供的内容，此处才生效@ConditionalMissingXxx 表示一定没有提供此内容，此配置才生效例如：@ConditionalOnBean , @ConditionalMissingBean实例代码：@Configuration //一旦IOC容器中未提供Car实例，则此处MyConfig生效 @ConditionalOnMissingBean(Car.class) public class MyConfig(){ }@Configuration //一旦IOC容器中提供Car实例，则此处MyConfig生效 @ConditionalOnClass(Car.class) public class MyConfig(){ }SpringBoot 2 底层源码+主启动类部分分析image-20230423203717288 此处内容已隐藏，回复后可见 此处内容已隐藏，回复后可见

SpringBoot自动配置原理（RedisAutoConfiguration）
type: item
date: 2023-04-23T15:18:00+08:00
description: RedisAutoConfiguration（Redis场景定制）@AutoConfiguration @ConditionalOnClass({RedisOperations.class}) @EnableConfigurationProperties({RedisProperties.class}) @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class}) public class RedisAutoConfiguration { public RedisAutoConfiguration() { } @Bean @ConditionalOnMissingBean( name = {"redisTemplate"} ) @ConditionalOnSingleCandidate(RedisConnectionFactory.class) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean @ConditionalOnSingleCandidate(RedisConnectionFactory.class) public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { return new StringRedisTemplate(redisConnectionFactory); } }RedisAutoConfiguration类上的注解一、@AutoConfiguration自动配置类注解是一个组合注解，组合中的注解有：@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration( proxyBeanMethods = false ) @AutoConfigureBefore @AutoConfigureAfter前三个元注解@Target( {ElementType.TYPE})该注解说明了注解修饰的对象范围，也就是注解可以使用的地方。其取值有：1、CONSTRUCTOR:用于描述构造器2、FIELD:用于描述域3、LOCAL_VARIABLE:用于描述局部变量4、METHOD:用于描述方法5、PACKAGE:用于描述包6、PARAMETER:用于描述参数7、TYPE:用于描述类、接口(包括注解类型) 或enum声明@Retention(RetentionPolicy.RUNTIME)该注解用来修饰注解的生命周期。其取值有：1、SOURCE：注解只保留在源文件，当Java文件编译成class文件的时候，注解被遗弃；2、CLASS：注解被保留到class文件，但jvm加载class文件时候被遗弃，这是默认的生命周期；3、RUNTIME：注解不仅被保存到class文件中，jvm加载class文件之后，仍然存在；@Documented该注解用于生成JavaDoc文档。其他三个注解@Configuration(proxyBeanMethods = false)该注解为配置类注解，是@Component注解的扩展注解，相当于xml配置文件，配置类本身也是组件。而且在这里使用的是轻加载。@AutoConfigureBefore一般配合@Component注解使用，表明在某类加载后再执行该配置类@AutoConfigureAfter表明在某类加载之前执行该配置类二、@ConditionalOnClass( {RedisOperations.class})该注解下有四个注解，前三个是元注解。@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional({OnClassCondition.class})@Conditional( {OnClassCondit…

Spring Security笔记
type: item
date: 2023-04-15T15:46:00+08:00
description: 权限设置问题http.authorizeRequests() //放行login .antMatchers("/checkLogin","/login.html").permitAll() .antMatchers("/pages/admin/**").hasRole("人事部主任") .antMatchers("/pages/bmAdmin/**").hasRole("部门主任") .antMatchers("/pages/emp/**").hasRole("部门员工") .antMatchers("/pages/renshi/**").hasRole("人事部员工");按上面设置后，项目接口可以无权限访问，需要添加一条.anyRequest().authenticated();所有请求都需要具有角色权限。当项目加入权限控制后，所有请求都会走一遍Spring Security，但如果当前发送请求的对象没有登陆时，会返回到设置的.failureForwardUrl("/index.html");上述路径中。如果已经登录，但无此请求所需权限，则会直接跳转到设置的403页面中。

IDEA Debug 功能
type: item
date: 2022-10-13T13:38:00+08:00
description: IDEA Debug 功能image-20220831144818923Show Execution Point：回到当前加载程序断点位置；Step Over：从当前断点逐步执行代码（运行到方法时直接跳过）；Step Into：加载测试方法并跳转到测试方法内部执行测试方法内部代码块，但是不会进入java类库的代码块；Force Step Into：但是不同于Step into 按钮的是当前 Force Step Into 按钮除了可以向下执行的同时，也能进入自定义方法和进入到java类库方法内部；Step Out：回退，每次回退都会回退到上层被调用的方法下方法；Run to Cursor：点击后，不进入断点；Evaluate Expression：计算器按钮，可以实现在不运行完成所有DeBug的情况下，实现数据运算，提前查看结果。

Java进程浅析+案例（初学者适用）
type: item
date: 2022-09-24T16:37:00+08:00
description: 笔记部分整理自 尚学堂.百战程序员 视频链接进程和线程的区别线程在进程中运行的。一个进程可以包含多个线程。不同进程间数据很难共享，而同一进程下不同线程间数据很易共享。进程要比线程消耗更多的计算机资源。进程间不会相互影响，因为它们的空间是完全隔离的。而进程中的一个线程挂掉将导致整个进程挂掉。进程使用的内存地址可以上锁，即一个线程使用某些共享内存时，其他线程必须等待结束才能使用这一块内存。线程的创建通过继承Thread类实现多线程在Java中负责实现线程功能的类是java.lang.Thread 类。继承Thread类实现多线程的步骤:继承Thread类定义线程类。重写Thread类中的run()方法。run()方法也称为线程体。实例化线程类并通过start()方法启动线程。子线程要在主线程中启动public class TestThread extends Thread{ public static void main(String[] args){ System.out.println("主线程开始"); //实例化线程类 TestThread t1 = new TestThread(); TestThread t2 = new TestThread(); //启动线程 //如果直接写t1.run(); 那么就在主线程中调用run方法 //所以我们需要使用start方法启动 t1.start(); t2.start(); //两个线程并发 //此时有三个线程：主线程和两个子线程 //程序运行->实例化线程类->子线程启动->主线程结束->子线程工作->程序结束 System.out.println("主线程结束"); } public TestThread(){ System.out.println(this.getName()); } //线程的线程体 @Override public void run(){ System.out.println(this.getName()+"线程开始"); for(int i=0;i<20;i++){ System.out.println(this.getName()+i); } System.out.println(this.getName()+"线程结束"); } }通过实现Runnable接口实现多线程public class TestThread implements Runnable{ public static void main(String[] args){ System.out.println("主线程开始"); TestThread t1 = new TestThread(); //启动线程 Thread t = new Thread(t1); t.start(); System.out.println("主线程结束"); } public TestThread(){ //这里会输出主线程名称 System.out.println(Thread.currentThread().getName()); } //线程的线程体 @Override public void run(){ System.out.println(Thread.currentThread().getName()+"线程开始"); for(int i=0;i<20;i++ ){ System.out.println(Thread.currentThread().getName()+" "+i); } System.out.println(Thread.currentThread().getName()+"线程结束"); } }线程的执行流程image-20220923204932996线程的生命周期image-20220923222647384一个线程对象在它的生命周期内，需要经历5个状态。新生状态(New)用new关键字建立一个线程对象后，该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间，通过调用start方法进入就绪状态。就绪状态(Runnable)处于就绪状态的线程已经具备了运行条件，但是还没有被分配到CPU，处于“线程就绪队列”，等待系统为其分配CPU。就绪状态并不是执行状态，当系统选定一个等待执行的Thread对象后，它就会进入执行状态。一旦获得CPU，线程就进入运行状态并自动调用自己的run方法。有四种原因会导致线程进入就绪状态:新建线程：调用start()方法，进入就绪状态;阻塞线程：阻塞解除,进入就绪状态;运行线程：调用yield()方法，直接进入就绪状态;运行线程：JVM将CPU资源从本线程切换到其他线程。运行状态(Running)在运行状态的线程执行自己run方法中的代…

Java内部类详解
type: item
date: 2022-09-22T22:00:00+08:00
description: 什么是内部类？类的五大成员属性、方法、构造方法、代码块、内部类内部类即在一个类的里面，再定义一个类。内部类表示的事物是外部类的一部分内部类单独出现没有任何意义内部类访问特点内部类可以直接访问外部类成员，包括私有外部类要访问内部类的成员，必须创建对象内部类分类成员内部类静态内部类局部内部类匿名内部类成员内部类卸载成员位置的，属于外部类的成员成员内部类可以被一些修饰符所修饰，比如：private，默认，protected，public，static等在成员内部类里面，JDK16之前不能定义静态变量，JDK16以后才可以public class Car{ //外部类 String carName; int carAge; int carColor; class Engine{ //成员内部类 String engineName; int engineAge; } }获取成员内部类对象在外部类中编写方法，对外提供内部类的对象。直接创建格式：外部类名.内部类名 对象名 = 外部类对象.内部类对象public class Outer{ String name; class Inner{ //JDK16之后才可以这样书写 static int num = 10; } public Inner getInstance(){ return new Inner(); } }public class Test{ public static void main(String[] args){ //直接创建内部类对象 //若成员内部类被private修饰，则无法直接创建内部类对象，只能通过在外部类中编写方法，对外提供内部类的对象。 Outer.Inner oi = new Outer.new Inner(); //另一种方法 //若Inner私有，则不能使用 Outer.Inner inner = o.getInstance(); //可以使用Inner的父类创建，形成多态，但这里Inner没有父类，默认继承Object Object inner = o.getInstance(); //对象地址值 System.out.println(o.getinstance()); } }成员内部类如何获取外部类的成员变量class Outer{ private int a = 10; class Inner{ private int a = 20; public void show(){ int a = 30; //Outer.this 获取了外部类对象的地址值 System.out.println(Outer.this.a); //10 System.out.println(this.a); //20 System.out.println(a); //30 } } }Outer.this.a 的原理：image-20220922203031111静态内部类是成员内部类的一种特殊情况静态内部类只能访问外部类中的静态变量和静态方法，如果想要访问非静态的需要创建对象。public class Car{ //外部类 String carName; int carAge; int carColor; static class Engine{ //静态内部类 String engineName; int engineAge; } }获取静态内部类对象创建静态内部类对象的格式： 外部类名.内部类名 对象名 = new 外部类名.内部类名();public class Test{ public static void mian(String[] args){ //创建静态内部类的对象 //只要是静态的东西，都可以直接用类名点直接获取 //非静态方法 Outer.Inner oi = new Outer.Inner(); oi.show1(); //静态方法 Outer.Inner.show2(); } }public class outer{ int a = 10; static int b = 20; static class Inner{ public void show1(){ System.out.println(a); //报错 Outer o = new Outer(); System.out.println(o.a); //正常 System.out.println(b); //正常 System.out.println("非静态的方法被调用"); } public static void show2(){ System.out.peintln("静态方法被调用"); } } }局部内部类将内部类定义在方法里面就叫做局部内部类，类似于方法里面的局部变量。外界是…

Java字符串拼接的底层原理
type: item
date: 2022-09-03T16:18:00+08:00
description: 拼接时无变量String s = "a" + "b" + "c";拼接的时候没有变量，都是字符串。触发字符串的优化机制。在编译的时候就已经是最终的结果了。即当Java文件编译成字节码文件（ .class）时，会检查是否有变量参与，如果没有，则直接生成最终结果。上述代码经编译后的结果为：String s = "abc";拼接时有变量在JDK8以前String s1 = "a"; String s2 = s1 + "b"; String s3 = s2 + "c";当上述代码运行时：首先s1="a"，会再StringTable串池里生成一个字符串"a"，再将串池中字符串"a"的地址给到s1；s2=s1+"b",串池中没有字符串"b"，故需要在串池中生成一个字符串"b"，该表达式中有变量，所以Java会自动在内存中创建一个StringBuilder对象，然后通过append()方法把s1和"b"都放到对象中，然后再将StringBuilder对象通过toString()方法转换成String对戏。s3=s2+"c"，与上相同，会再创建一个StringBuilder对象，使用append()方法做拼接。整个过程相当于：new StringBuilder().append(s1).append("b").toString();通过分析StringBuiler的源码，可知字符串和变量一次拼接，内存中至少会有两个对象，一个是StringBuilder对象，一个是String对象。image-20220903160211455由此看出这样拼接，速度很慢，浪费内存。JDK8及以后String s1 = "a"; String s2 = "b"; String s3 = "c"; String s4 = s1 + s2 + s3;运行上述程序后，Java会根据s1、s2、s3预估s4的长度，然后创建一个数组，若为上述所示代码，会创建一个长度为3的数组，将"a","b","c"放入数组中，然后将数组变为一个字符串。但当有多个变量参与拼接时，预估最终字符串长度会耗费很多时间，在内存中也创建了很多对象，也会浪费空间。所以若有很多字符串变量拼接，不要直接+，而是使用StringBuilder或者StringJoiner。易混淆点String s1 = "abc"; String s2 = "ab"; String s3 = s2 + "c"; System.out.println(s1==s3);输出结果应为：false不管在JDK8之前还是在JDK8，字符串拼接时最终都会new一个String对象出来。JDK8之前：调用StringBuilder对象-->append()方法拼接-->toString方法（toString方法底层实际是new了一个String对象）JDK8：预估最终字符串长度-->将要拼接内容放入数组-->产生新的String对象String s1 = "abc"; String s2 = "a" + "b" + "c"; System.out.println(s1==s2);输出结果应为：trues2在编译时就已经被拼接完成了，所以s1，s2在串池中的地址值相同，输出true。