SpringBoot

微服务阶段

javaSE:OOP(面向对象);

mysql:持久化;

html-css-js-jquery+框架:视图,框架不熟练,css不好;

javaweb:独立开发mvc三层架构的网站;

ssm:框架,简化了开发流程,,但是配置也开始复杂了;

war:tomcat运行

SpringBoot:微服务架构!内嵌tomcat!

springcloud:微服务越来越多!

新服务架构:服务网格

alt

==约定大于配置==:maven、spring、SpringBoot、springMVC.docker、k8s

spring

Spring是一个开源框架,2003年兴起的一个轻量级java开发框架,作者:Rod Johnson。Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。

spring如何简化开发的?

  • 基于POJO的轻量级和最小入侵性编程
  • 通过IOC,依赖注入(DI)和面向接口实现松耦合
  • 基于切面(AOP)和惯例进行声明式编程
  • 通过切面和模板减少样式代码

springboot的优点

  • 为所有spring开发者提1+2
  • 供更快的入门
  • 开箱即用,提供各种默认配置来简化项目配置
  • 内嵌式容器简化Web项目
  • 没有冗余代码生成和XML配置的要求

springcloud

  • 构建一个个功能独立的微服务应用单元,可以使用springboot;
  • 大型分布式网络服务的调用由SpringCloud实现分布式;
  • 在分布式中间,通过spring cloud data flow进行流式数据计算、批处理;

第一个springboot程序

1. jdk1.8
2. maven 3.6.1
3. springboot 最新
4. IDEA

官方提供了一个快速生成的网站!IDEA集成了这个网站!

alt

https://start.spring.io/

alt

一般用idea创建

alt

如果出现报错

Initialization failed for 'https://start.spring.io' Please check URL, network and proxy settings.

则可以使用阿里云的 https://start.aliyun.com/

注意:创建springboot项目时用了阿里云的镜像是没有parent标签的,用springboot自身的创建项目是有parent标签的

alt

  • 项目元数据信息:创建的时候输入的Project Metadata部分,也就是maven项目的基本元素,包括groupid、artifactid、version、name、description等;
  • parent:集成==spring-boot-starter-parent==的依赖管理,控制版本与打包等内容;
  • dependencies:项目的具体依赖,这里包含了==spring-boot-starter-web==用于实现HTTP接口(该依赖中包含了Spring MVC),官网对他的描述是:使用spring mvc构建web(包括RESTful)应用程序的入门者,使用tomcat作为默认嵌入式容器,==spring-boot-starter-test==用于编写单元测试的依赖包;
  • build:构建配置部分,默认使用==spring-boot-maven-plugin==,配合spring-boot-starter-parent就可以把spring boot应用打包成JAR来直接运行。

原理初探

自动配置:

pom.xml

  • spring-boot-dependencies:核心依赖早父工程中!
  • springboot有版本仓库,所以一般不需要指定一些springboot依赖的版本,除非版本仓库的版本不是我们需要的,这个时候需要重置一下版本,方式是在pom文件中新增如下:
<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>

启动器:

  • <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    
  • 启动器说白了就是springboot的启动场景;

  • 比如spring-boot-starter-web,他就会帮我们自动导入web环境所有的依赖!

  • springboot会将所有的功能场景都变成一个个启动器!

  • 我们要使用什么功能,就使用相应的启动器 starter!!!!参见

    https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters

主程序

//标注这个类是一个springboot的应用:启动类下的所有资源被导入
@SpringBootApplication
public class HelloworldApplication {
    public static void main(String[] args) {
        //将springboot应用启动
        SpringApplication.run(HelloworldApplication.class, args);
    }
}

注解

@SpringBootConfiguration:springboot的配置
	@Configuration
		@Component
@EnableAutoConfiguration://自动配置
    @AutoConfigurationPackage //自动配置包
    	@Import({Registrar.class}) //自动配置包注册
    @Import({AutoConfigurationImportSelector.class})  //自动配置导入选择器
    	//获取所有的配置
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);

获取候选的配置

//org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getCandidateConfigurations
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                                                                         getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
                    + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
    return EnableAutoConfiguration.class;
}
/**
class:SpringFactoriesLoader
*/
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    String factoryTypeName = factoryType.getName();
    return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
    if (result != null) {
        return result;
    } else {
        try {
            Enumeration<URL> urls = classLoader != null ? 
                classLoader.getResources("META-INF/spring.factories") 
                : ClassLoader.getSystemResources("META-INF/spring.factories");
            LinkedMultiValueMap result = new LinkedMultiValueMap();

            while(urls.hasMoreElements()) {
                URL url = (URL)urls.nextElement();
                UrlResource resource = new UrlResource(url);
                //所有资源都加载到配置类中
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                Iterator var6 = properties.entrySet().iterator();

                while(var6.hasNext()) {
                    Entry<?, ?> entry = (Entry)var6.next();
                    String factoryTypeName = ((String)entry.getKey()).trim();
                    String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                    int var10 = var9.length;

                    for(int var11 = 0; var11 < var10; ++var11) {
                        String factoryImplementationName = var9[var11];
                        result.add(factoryTypeName, factoryImplementationName.trim());
                    }
                }
            }

            cache.put(classLoader, result);
            return result;
        } catch (IOException var13) {
            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
        }
    }
}

==META-INF/spring.factories==:自动配置的核心文件

alt

所有资源都加载到配置类中 Properties properties = PropertiesLoaderUtils.loadProperties(resource);

结论:springboot所有自动配置都是在启动的时候扫描并加载:spring.factories所有的自动配置类都在这里面,但不一定生效,要判断条件是否成立,只要导入了对应的starter,就有了相应的启动器了,有了启动器,我们自动装配才会生效,然后就配置成功!

  1. springboot在启动的时候,从类路径下/META-INF/factories获取指定的值;
  2. 将这些自动配置的类导入到容器,自动配置就会生效,帮我们进行自动配置;
  3. 整合javaEE,解决方案和自动配置的东西都在spring-boot-autoconfigure-2.3.7.RELEASE.jar这个包下;
  4. 他会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器;
  5. 容器中也会存在非常多的xxxAutoConfiguration的文件,就是这些类给容器中导入了这个场景需要的所有组件,并自动配置,@Configuration,javaconfig!
  6. 有了自动配置类,免去了我们手动编写配置注入功能组件等的工作! alt

启动main方法

main方法运行并开启了一个服务

@SpringBootApplication
public class HelloworldApplication {
    public static void main(String[] args) {
        //该方法返回一个ConfigurableApplicationContext对象  
        //参数:应用入口类,   命令行参数
        SpringApplication.run(HelloworldApplication.class, args);
    }
}

该方法主要分为两部分,一部分是SpringApplication的实例化,另一部分是run方法的执行

SpringAppplication:

1.推断应用的类型是普通项目还是web项目;
2.查找并加载所有可用初始化器,设置到initializers属性中;
3.找出所有的应用程序监听器,设置到listeners属性中;
4.推断并设置main方法的定义类,找到运行的主类。
//查看构造器
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.sources = new LinkedHashSet();
    this.bannerMode = Mode.CONSOLE;
    this.logStartupInfo = true;
    this.addCommandLineProperties = true;
    this.addConversionService = true;
    this.headless = true;
    this.registerShutdownHook = true;
    this.additionalProfiles = new HashSet();
    this.isCustomEnvironment = false;
    this.lazyInitialization = false;
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = this.deduceMainApplicationClass();
}

run

alt

public ConfigurableApplicationContext run(String... args) {
    	//计时器启动
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
        this.configureHeadlessProperty();
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting();

        Collection exceptionReporters;
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
            Banner printedBanner = this.printBanner(environment);
            context = this.createApplicationContext();
            exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            this.refreshContext(context);
            this.afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }

            listeners.started(context);
            this.callRunners(context, applicationArguments);
        } catch (Throwable var10) {
            this.handleRunFailure(context, var10, exceptionReporters, listeners);
            throw new IllegalStateException(var10);
        }

        try {
            listeners.running(context);
            return context;
        } catch (Throwable var9) {
            this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var9);
        }
    }

关于springboot,谈谈理解?

  • 自动装配
# 注解  
  • run:
1.推断应用的类型是普通项目(启动完毕就结束)还是web项目(可以一直启动);
2.推断并设置main方法的定义类,找到运行的主类(不知道主类是没有办法加载的);
3.有一些全局的监听器,获取上下文,处理bean

配置文件以及自动装配原理

官方的配置,2.7.0版本路径如下

https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties.devtools

配置文件

springboot使用一个全局的配置文件,名称是固定的

  • application.properties 语法结构为 key=value
  • application.yaml 语法结构为 key:空格 value(==建议使用==)

配置文件作用:修改springboot自动配置的默认值,因为springboot在底层都给我们自动配置好了。

#普通的key-value
server:
  port: 8081
# 对象
student: 
  name: zzm
  age: 29
#行内对象写法
student: {name: zzm,age: 29}
#数组
animals: 
  - cat
  - dog
  - pig
#行内数组写法
animals: [cat,dog,pig]

如果使用properties文件的话,idea要设置编码为utf-8

alt

案例1 通过yaml文件注入一个对象

注意:==使用注解@ConfigurationProperties时,要导入包,否则会提示,但是不影响程序的运行==

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
  1. yaml文件(yaml文件中还可以使用占位符${}以及逻辑判断的${:})
person:
  name: zzm${random.int}
  age: 29
  happy: true
  birth: 1993/11/20
  maps: {k1: v1,k2: v2}
  lists:
    - code
    - music
    - girl
  dog:
    name: ${person.hello:kaka}比鲁斯
    age: 3
  1. Person类和Dog类
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Component
@ConfigurationProperties(prefix = "person")
public class Person implements Serializable {
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String, Object> maps;
    private List<Object> lists;
    private Dog dog;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Component
public class Dog implements Serializable {

    private String name;
    private int age;
}
  1. 调用person对象
@Autowired
Person person;

@Test
void contextLoads() {
    System.out.println(person);
}

案例2 通过properties文件注入一个对象

  1. zidingyi.properties文件
person.name=卡卡罗特
person.age=14
  1. person类
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@PropertySource("classpath:zidingyi.properties")
public class Person implements Serializable {
    
    //取出配置文件的值
    @Value("${person.name}")
    private String name;
    //SPEL表达式
    @Value("#{11*2}")
    private Integer age;
    //字面量
    @Value("true")
    private Boolean happy;
}

两种配置方式的区别

@ConfigurationProperties @Value
功能 批量注入配置文件中的属性 一个个指定
松散绑定(松散语法) 支持 不支持
SPEL 不支持 支持
JSR303数据校验 支持 不支持
复杂类型封装 支持 不支持

附:

  1. 松散绑定:比如yaml中的last-name和lastName是一样的,-后面跟的字母默认是大写的;
  2. JSR303数据校验:在字段上增加一层过滤器验证,可以保证数据的合法性。

2.1. 导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2.2. 配置到实体类

import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Email;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Component
@ConfigurationProperties(prefix = "person")
@Validated
public class Person implements Serializable {
    private static final long serialVersionUID = 281903912367009575L;

    @Email(message="姓名格式不正确")
    private String name;
    private Integer age;
}

alt

Hibernate 中填充一部分

alt

注解                      注解详情
@Null                  被指定的注解元素必须为 Null
@NotNull               任意类型,不能为 Null,但可以为空,比如空数组、空字符串。
@NotBlank              针对字符串,不能为 Null,且去除前后空格后的字符串长度要大于 0。
@NotEmpty              针对字符串、集合、数组,不能为 Null,且长度要大于 0。
@Size                   针对字符串、集合、数组,判断长度是否在给定范围内。
@Length                 针对字符串,判断长度是否在给定范围内。
@AssertTrue             针对布尔值,用来判断布尔值是否为 true
@AssertFalse            针对布尔值,用来判断布尔值是否为 false
@Past                  针对日期,用来判断当前日期是否为 过去的日期
@Future                针对日期,用来判断当前日期是否为 未来的日期
@Max(value)         针对字符串、数值,用来判断是否小于等于某个指定值
@Min(value)         针对字符串、数值,用来判断是否大于等于某个指定值
@Pattern             验证字符串是否满足正则表达式
@Email               验证字符串是否满足邮件格式
@Url                 验证是否满足 url 格式

配置文件位置

alt

以上四个位置都可以被读到

优先级按照序号,默认是优先级最低的,会被优先级高的地方覆盖(这应该是配置可以集中由三方服务管理的原因:比如disconf、Apollo等)。

springboot的多环境配置

  1. 方式1:多个配置文件

#application.properties文件
#springboot的多环境配置:可以激活某一个配置文件
spring.profiles.active=test
server.port=8080

#application-test.properties
server.port=8081
#application-product.properties
server.port=8082
  1. 方式2
server: 
  port: 8080
spring: 
  profiles: 
    active: dev
    
---
server: 
  port: 8081
spring: 
  profiles: dev

---
server: 
  port: 8082
spring: 
  profiles: pro

---
server: 
  port: 8083
spring: 
  profiles: test

配置文件内容

#配置文件能写什么? ---联系---spring.factories

#规则:
# xxxAutoConfiguration:默认值  xxxProperties 和配置文件绑定,我们就可以使用自定义的配置了

#理解: spring.factories下的配置类xxxAutoConfiguration会加载配置属性类xxxProperties,设置默认值,如果我们
#需要重新设置值,则在配置文件中按照松散绑定编写相应的变量即可。

总结自动装配的精髓:

  1. springboot启动会加载大量的自动配置类;
  2. 判断我们需要的功能是否在springboot默认写好的自动配置类中;
  3. 查看自动配置类中配置了哪些组件;(如果使用的组件存在于自动配置类中,那我们不需要再手动配置了)
  4. 给容器中的自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;
  5. xxxAutoConfiguration:自动配置类 ;xxxProperties:封装配置文件中相关属性。

==注意==

自动配置类必须在一定条件下才能生效:@Conditionxxx注解(Spring的@Condition注解的派生注解)

@Conditional 作用(判断是否满足当前指定条件)
@ConditionalOnJava 系统的java版本是否符合要求
@ConditionalOnBean 容器中存在指定Bean;
@ConditionalOnMissingBean 容器中不存在指定Bean;
@ConditionalOnExpression 满足SpEL表达式指定
@ConditionalOnClass 系统中有指定的类
@ConditionalOnMissingClass 系统中没有指定的类
@ConditionalOnSingleCandidate 容器中只有一个指定的Bean,或者这个Bean是首选Bean
@ConditionalOnProperty 系统中指定的属性是否有指定的值
@ConditionalOnResource 类路径下是否存在指定资源文件
@ConditionalOnWebApplication 当前是web环境
@ConditionalOnNotWebApplication 当前不是web环境
@ConditionalOnJndi JNDI存在指定项

查看哪些配置类是生效的

#application.yaml文件中增加配置
debug: true

启动应用,查看日志即可,分为三种:

  1. Positive matches 自动配置类启用的:正匹配
  2. Negative matches 没有启动,没有匹配成功的自动配置类:负匹配
  3. Exclusions 排除掉的
  4. Unconditional classes 没有条件的类

SpringBoot Web开发

解决问题:

  • 导入静态资源
  • 首页
  • jsp,模板引擎 Thymeleaf
  • 装配扩展SpringMvc
  • 增删改查
  • 拦截器
  • 国际化

静态资源

public void addResourceHandlers(ResourceHandlerRegistry registry) {
    
    //首先判断是否自定义配置
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    
    //其次,查看webjars  官网导入所需maven依赖,对应相应的jar包中的/META-INF/resources/webjars/路径
    Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
    CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
    if (!registry.hasMappingForPattern("/webjars/**")) {
        customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                                             .addResourceLocations("classpath:/META-INF/resources/webjars/")
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
    
    //第二种就是经常见到的路劲,所有的/开头的,都会从下面四个路径中匹配,优先级就是下面的顺序
    // staticPathPattern => "/**"
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();  
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
  //this.resourceProperties.getStaticLocations() => { "classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/" }
                                             .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
}

总结:

  1. 在springboot中,我们可以使用一下方式处理静态资源
    • webjars localhost:8080/webjars/
    • public, static, reosurce localhost:8080/
  2. 优先级:public (公共的文件)< static (js等静态资源)<resource(上传文件)

获取首页

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
                                                           FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
        new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
        this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
    return welcomePageHandlerMapping;
}

private Optional<Resource> getWelcomePage() {
//{ "classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/" }
    String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
    return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}

private Resource getIndexHtml(String location) {
    return this.resourceLoader.getResource(location + "index.html");
}

注意:在template目录下的所有页面,只能通过controller跳转

模板引擎

模板引擎有很多,包括js、freemarker以及springboot推荐的Thymeleaf。

Thymeleaf

官网:https://www.thymeleaf.org/

github:https://github.com/thymeleaf/thymeleaf

  1. 导入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  1. java类增加传递参数
//model 和 maps同时生效,只传一个也可以,如果出现同一个参数msg赋值,后面的会覆盖前面的
@RequestMapping("/index")
public String index(Model model, Map<String,Object> maps){
    maps.put("msg","hello,everyone33!");
    model.addAttribute("msg","hello,everyone!");
    model.addAttribute("users", Arrays.asList("卡卡罗特","贝吉塔","布罗利"));
    model.addAttribute("user",new User("卡卡罗特别强",22));
    maps.put("msg3","hello,everyone3dfgds3!");
    return "index";
}
  1. html增加名称空间
<!DOCTYPE html>
<!--导入thymeleaf的名称空间-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>首页</h1>
    <h2 th:text="${msg}"></h2>

    <h2 th:text="${msg3}"></h2>

    <h3 th:each="user:${users}" th:text="com.wenyibi.futuremail.model.User@1167a47f"></h3>
    
    <!--这种不做推荐使用-->
    <h3 th:each="user:${users}" >[[ com.wenyibi.futuremail.model.User@1167a47f ]]</h3>   
    
    <!--如果有指定对象th:object,则里面不能使用$,只能使用*,这就是*和$的区别 -->
    <div th:object="com.wenyibi.futuremail.model.User@1167a47f">
        <p>Name: <span th:text="${name}"></span>.</p>   
        <p>Age: <span th:text="*{age}"></span>.</p>
    </div>
</body>
</html>
  1. 结果

alt

//这个配置参数类解释了导入thymeleaf包后,controller可以直接链接到template目录下的html文件

@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {

	private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;

	public static final String DEFAULT_PREFIX = "classpath:/templates/";

	public static final String DEFAULT_SUFFIX = ".html";
    

//在此处加载Thymeleaf的配置参数类
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnMissingBean(name = "defaultTemplateResolver")
	static class DefaultTemplateResolverConfiguration {

在springboot中,有非常多的xxxConfiguration帮助我们进行扩展配置,只要看见这个东西,我们就要注意了!

实战1-员工管理系统

自定义视图解析器

//@EnableWebMvc这个加上自定义视图不生效
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("signin");
        registry.addViewController("/index").setViewName("signin");
        registry.addViewController("/index.html").setViewName("signin");
    }
}

前端模板下载

https://sc.chinaz.com/tag_moban/bootstrap.html

首页配置

首页配置:所有页面的静态资源都需要用Thymeleaf接管;url : @{}

页面国际化

//org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
public LocaleResolver localeResolver() {
    if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
        return new FixedLocaleResolver(this.mvcProperties.getLocale());
    }
    AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
    localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
    return localeResolver;
}
  1. 配置i18n文件;

alt

  1. 如果需要项目通过按钮自动切换,需自定义组件LocaleResolver;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

public class MyLocaleResolver implements LocaleResolver {

    /**
     * 解析请求
     * @param request
     * @return
     */
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        String language = request.getParameter("l");
        //zh_CN    en_US
        if (!StringUtils.isEmpty(language)) {
            String[] splits = language.split("_");
            //language  region
            return new Locale(splits[0],splits[1]);
        }
        return Locale.getDefault();
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}
  1. 将自己写的组件配置到spring容器中 @Bean
//@EnableWebMvc这个加上自定义视图不生效
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("signin");
        registry.addViewController("/index").setViewName("signin");
        registry.addViewController("/index.html").setViewName("signin");
    }

    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
}

登录+拦截器

@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Model model, HttpSession session){
        if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password) && "67890".equals(password)){
            model.addAttribute("msg","登录成功!");
            session.setAttribute("loginStatus",1);
            return "redirect:/index.html";
        }
        if(!(StringUtils.isEmpty(username) && StringUtils.isEmpty(password))){
            model.addAttribute("msg","用户名或密码错误!");
        }
        return "signin";
    }
}

//登录拦截器
public class LoginHandlerInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object loginStatus = request.getSession().getAttribute("loginStatus");
        if(loginStatus == null){
            request.setAttribute("msg","没有权限,请先登录");
            request.getRequestDispatcher("/signin").forward(request,response);
            return false;
        }
        return true;
    }
}

//添加自定义拦截器 com.karottezzm.springboot04web.config.MyMvcConfig
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LoginHandlerInterceptor())
        .addPathPatterns("/**").excludePathPatterns("/signin","/","/user/login","/css/**","/js/**","/img/**");
}

展示列表

提取公共页面

1. <nav class="navbar bg-light navbar-light" th:fragment="sidebar">
2. <div th:replace="~{commons/common::sidebar}"></div>
3. 如果要传递参数,可以直接使用()传参,接收判断即可
    <div th:replace="~{commons/common::sidebar(act='table')}"></div>
    
    <a th:href="@{index1.html}" th:class="'nav-item nav-link active':'nav-item nav-link'">

编辑删除按钮需要拼接字符串

<td>
    <a class="btn btn-sm btn-primary" th:href="@{/employee/toEdit/}+${emp.getId()}" >编辑</a>
    <a class="btn btn-sm btn-danger" th:href="@{/employee/del/}+${emp.getId()}">删除</a>
</td>

404:将错误页面放到templates/error/目录下即可

alt

后端模板

http://x.xuebingsi.com/ xadmin

Data

对于数据访问层,无论是SQL还是NOSQL,springboot底层采用的都是Spring Data的方式进行统一处理。

官网:https://spring.io/projects/spring-data

JDBC

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver  #与mysql版本相关  8.0为分界   小于,则com.mysql.jdbc.Driver
@RestController
@RequestMapping("/jdbc")
public class JDBCController {
    @Autowired
    JdbcTemplate jdbcTemplate;
    @Autowired
    DataSource dataSource;

    @RequestMapping("/list")
    public List<Map<String,Object>> getList(){
        String sql = "select * from user ";
        List<Map<String,Object>> result = jdbcTemplate.queryForList(sql);
        return result;
    }
    
    public void contextLoads() throws SQLException {
        System.out.println(dataSource.getClass());
        Connection connection = dataSource.getConnection();
        System.out.println(connection);
        connection.close();
    }
}

DRUID

Druid是阿里巴巴开源平台上一个数据库连接池实现,结合了C3P0、DBCP、PROXOOL等DB、池的优点,同时加入了==日志监控==。

Springboot2.0以后默认使用Hikari数据源,可以说Hikari和Druid都是当前Java Web上最优秀的数据源。

springboot低版本使用的数据源为org.apache.tomcat.jdbc.pool.DataSource.

==高版本的为HikariDataSource,号称Java Web当前速度最快的数据源。==

http://mvnrepository.com/artifact/com.alibaba/druid

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    
    #springboot默认不注入这些属性值,需要自己绑定
    #druid数据源专用绑定
    initialSiza: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMills: 60000
    MinEvictableIdleTimeMills: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    test-on-borrow: false
    test-on-return: false
    pool-prepared-statements: true
    
    #配置监控统计拦截的filters、 stat:监控统计、log4j:日志记录、wall:防御SQL注入
    #如果运行时报错,java.lang.ClassNotFoundException:org.apache.log4j.Priority
    #则导入log4j依赖即可,Maven地址:https://mvnrepository.com/artifact/log4j/logj
    
    filters: stat,wall,log4j
    max-pool-prepared-statement-per-connection-size: 20
    use-global-data-source-stat: true
    connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMills=500

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DruidConfig{
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }
    //后台监控
    //因为springboot内置了servlet容器,所以所以没有web.xml  替换方法:ServletRegistrationBean
    @Bean
    public ServletRegistrationBean statViewServlet(){
        ServletRegistrationBean<StatViewServlet> bean = 
            //注意,这里如果写成"/druid/**"就会报404
            new ServletRegistrationBean<>(new StatViewServlet(),"/druid/*");
        //后台系统需要有人登录,设置账号密码
        HashMap<String,String> initParameter = new HashMap<>();
        //下面两个字段固定
        initParameter.put("loginUsername","admin");
        initParameter.put("loginPassword","admin");
        initParameter.put("allow","");//默认就是允许所有访问
        //initParameter.put("deny","192.168.15.21");
        bean.setInitParameters(initParameter);//初始化参数
        return bean;
    }
    //2、配置一个web监控的filter
    @Bean
    public FilterRegistrationBean webStatFilter(){
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new WebStatFilter());
        Map<String,String> initParams = new HashMap<>();
        //这些路径不会统计
        initParams.put("exclusions","*.js,*.css,/druid/*");
        bean.setInitParameters(initParams);
        bean.setUrlPatterns(Arrays.asList("/*"));
        return  bean;
    }
}

alt

alt

mybatis

整合包:

mybatis-spring-boot-starter

pom.xml文件

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  type-aliases-package: com.kaka.pojo
  mapper-locations: classpath:mybatis/mapper/*.xml

mapper.xml内容格式参见:https://mybatis.net.cn/getting-started.html

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">


<mapper namespace="com.kaka.mapper.UserMapper">
    <select id="queryUserList" resultType="User" useCache="true">
        select * from user
    </select>
</mapper>

SpringSecurity(安全)

截止2022/5/28

alt

简介

SpringSecurity是针对Spring项目的安全框架,也是springboot底层安全模块默认的技术选型,他可以实现强大的web安全控制,而我们只需引入spring-boot-starter-security模块,进行少量的配置。

关键类

  • WebSecurityConfigurerAdapter:自定义安全策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启webSecurity模式

SpringSecurity的主要目标就是 认证(Authentication)和授权(authorization,也可以说是访问控制)

参考官网:https://spring.io/projects/spring-security#learn

与thymeleaf整合

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
    <version>3.0.4</version>
</dependency>
<html lang="en" xmlns:th="http://www.thymeleaf.org" 
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">

shiro

关键类

  1. Subject
  2. SecurityManager
  3. Realm :授权(doGetAuthenticationInfo) 认证(doGetAuthorizationInfo)
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.setAttribute("loginUser",user);
currentUser.isAuthenticated();
currentUser.getPrincipal();
currentUser.hasRole("user");
currentUser.isPermitted("user:add");
currentUser.logout();
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.9.0</version>
</dependency>

与thymeleaf整合

<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.1.0</version>
</dependency>
<html lang="en" xmlns:th="http://www.thymeleaf.org" 
      xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
    
    <div th:if="${session.loginUser==null}">
    	<a th:href="@{/toLogin}">登录</a>
    </div>   
    
    <div shiro:hasPermission="user:add">
    	<a th:href="@{/user/add}">x新增</a>
    </div>  
    
    <div shiro:hasPermission="user:update">
    	<a th:href="@{/user/update}">更新</a>
    </div> 

Swagger

  • 作用和概念

  • 前后端分离

  • SpringBoot集成swagger

swagger简介

前后端分离

vue+SpringBoot

后端时代:前端只负责管理静态页面;html==》后端。模板引擎 jsp

前后端分离时代:

  • 后端:后端控制层、服务层、数据访问层
  • 前端:前端控制层、视图
  • 交互:API
  • 前端测试后端接口:postman Apifox(联网可共享)
  • 后端提供接口,需要实时更新最新的消息及改动。

swagger

  • RestFul Api文档在线自动生成工具=》==Api文档与Api定义同步更新==
  • 直接运行,可以在线测试API接口
  • 支持多种语言
  • 官网:https://swagger.io/

项目中的swagger

在项目中使用swagger需要springbox:

  • swagger2
  • ui

SpringBoot集成swagger

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>
@Configuration
@EnableSwagger2  //开启swagger2
public class SwaggerConfig {
    
}

访问:http://localhost:8080/swagger-ui.html

alt

==上述就是最简单的使用==,接口中返回中有实例的时候,model中才会出现

配置Swagger

@Configuration
@EnableSwagger2  //开启swagger2
public class SwaggerConfig {
    /**
     * 配置swagger的实例
     * @return
     */
    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo());
    }
    private ApiInfo apiInfo(){
        Contact con = new Contact("卡卡", "https://blog.nowcoder.net/karottezzm", "wuqing@163.com");//作者信息
        return new ApiInfo("卡卡罗特别强", "醉卧沙场君莫笑", "v1.0", "https://blog.nowcoder.net/karottezzm",
                con, "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", new ArrayList<>());
    }

alt

自定义扫描接口



@Value("${swagger.enable}")
private boolean enbale;

@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .enable(true)//是否启用swagger  根据enbale实现多个环境不同配置swagger开关
        .select()
        //RequestHandlerSelectors配置要扫描接口的方式
        //any():全部
        //none():不扫描
        //withClassAnnotation:扫描类上的注解,参数是一个注解的反射对象
        //withMethodAnnotation:扫描方法上的注解
        //basePackage:包扫描
        .apis(RequestHandlerSelectors.withMethodAnnotation(Api.class))
        //path  过滤什么路径
        .paths(PathSelectors.none())
        .build();
}
spring:
  thymeleaf:
    cache: false
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
  profiles:
    active: pro
mybatis:
  type-aliases-package: com.kaka.pojo
  mapper-locations: classpath:mybatis/mapper/*.xml
---
spring:
  profiles: test
swagger:
  enable: false
---
spring:
  profiles: pro
swagger:
  enable: true
---
spring:
  profiles: dev
swagger:
  enable: false

==也可以通过自定义注解横向配置swagger==

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface SwaggerMethod{}

@SwaggerMethod
@ApiOperation("获取用户信息列表")
@GetMapping("/list")
public List<User> getUserList(){
    return userMapper.queryUserList();
}

@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .enable(enbale)//是否启用swagger
        .groupName("super赛亚人")
        .select()
        //RequestHandlerSelectors配置要扫描接口的方式
        //any():全部
        //none():不扫描
        //withClassAnnotation:扫描类上的注解,参数是一个注解的反射对象
        //withMethodAnnotation:扫描方法上的注解
        //basePackage:包扫描
        .apis(RequestHandlerSelectors.withMethodAnnotation(SwaggerMethod.class))
        //.apis(RequestHandlerSelectors.basePackage("com.kaka.controller"))
        //path  过滤什么路径
        //.paths(PathSelectors.none())
        .build();
}

任务

异步任务

@Service
public class UserService {
    /**
     * 测试异步任务
     */
    @Async
    public void sayHello(){
        try {
            Thread.sleep(3000);
        }catch (Exception e){
        }
    }
}

/////////////////////////@EnableAsync 调用者要添加该注解
@Controller
@EnableAsync
public class IndexController {
    @Autowired
    UserService userService;

    @SwaggerMethod
    @GetMapping({"/","index","index.html"})
    public String index(){
        userService.sayHello();
        return "index";
    }
}

邮件任务

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
spring: 
  mail:
    username: 14536595848@163.com
    password: hsudha987678hhasjkh
    properties:
      mail:
        smtp:
          ssl:
            enable: true
    host: smtp.11.com
@Autowired
JavaMailSenderImpl mailSender;

@Autowired
UserMapper userMapper;

@SwaggerMethod
@ApiOperation("获取用户信息列表")
@GetMapping("/list")
public List<User> getUserList(){
    SimpleMailMessage message = new SimpleMailMessage();
    message.setFrom("wuqingzzm@163.com");
    message.setTo("wuqingzzm@163.com");
    message.setSubject("lalala");
    message.setText("通知一下而已");
    mailSender.send(message);
    return userMapper.queryUserList();
}

定时任务

TaskSchduler  #任务调度
TaskExecutor  #任务执行

@EnbaleScheduling     #开启定时功能的注解  一般放到入口

@Scheduled  #执行时间

cron表达式
@SpringBootApplication
@EnableAsync
@EnableScheduling
public class SpringbootSwaggerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootSwaggerApplication.class, args);
    }
}


@Async
@Scheduled(cron = "0 * * * * ?")
public void sayHello(){
    try {
        System.out.println("执行蒂诺故事人物");
    }catch (Exception e){
    }
}

分布式 + Dubbo + Zookeeper

all in -> mvc -> rpc -> soa

分布式系统:distributed system

RPC: remote procedure call 远程过程调用,进程间通信方式 核心:通讯+序列化

SOA:service oriented architecture 面向服务的架构 ==资源调度和治理中心是关键==

alt

dubbo: 是一个jar包,引入jar包即可

dubbo-admin:是一个监控管理后台,可以查看服务注册情况和消费情况 7001端口

zookeeper:2181端口

<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>3.0.8</version>
</dependency>
<dependency>
    <groupId>com.github.sgroschupf</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.1</version>
</dependency>

<!-- 引入zookeeper日志会冲突 -->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>5.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>5.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.8.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>
#提供服务
dubbo:
  application:
    name: provider-server
  registry:
    address: zookeeper://127.0.0.1:2181
  scan:
    base-packages: com.kaka.service.rpc
#消费服务  
dubbo:
  application:
    name: consume-server
  registry:
    address: zookeeper://127.0.0.1:2181
 
import org.springframework.stereotype.Component;
import org.apache.dubbo.config.annotation.Service;
@Service
@Component
public class UserService { //提供服务
    

import org.springframework.stereotype.Service;
@Service
public class UserService { //消费服务    

微服务架构的问题?

分布式架构会遇到的四个核心问题:

  1. 大量的服务,客户端如何访问;
  2. 大量的服务,服务之间如何通信;
  3. 大量的服务,如何治理;
  4. 服务挂了怎么办?

解决方案:

​ springcloud,一套生态,就是来解决以上分布式架构的4个问题。

  1. Spring Cloud Netflix 出来了一套解决方案!一站式解决方案,我们都可以直接去这里拿?
    • Api网关 zuul组件
    • Feign --> httpclient ---> HTTP的通信方式,同步并阻塞
    • 服务注册与发现 Eureka
    • 熔断机制 Hystrix

​ 2018年底,NetFlix宣布巫期限停止维护。生态不再维护,就会脱节。

  1. Apache Dubbo zookeeper 第二套解决方案

    • API:没有,要么找三方组件,要么自己实现
    • Dubbo是一个高性能的基于java实现的RPC通信框架
    • 服务注册与发现:zookeeper-动物管理员(Hadoop,Hive)
    • 没有,借助Hystrix
  2. ApacheCloud Alibaba 一站式解决方案

  3. 服务网格:下一代微服务标准,Server Mesh

    代表解决方案:istio

最终解决的问题:

  1. API网关,服务路由;
  2. HTTP,RPC框架,异步调用;
  3. 服务注册与发现,高可用;
  4. 熔断机制,服务降级

以上内容如有错误之处,欢迎指正!!!

学习源为:https://space.bilibili.com/95256449/channel/seriesdetail?sid=393820&ctype=0