spring编程框架

spring boot, spring data, spring framework

Spring

@Profile('prod'|'dev'|'other')(伴随@Bean)特定profile下的bean (激活profile的配置: spring.profiles.active=prod, 对应自动加载的配置文件application-prod.properties)
@Profile('dev') 开发模式
@Profile('prod') 生产模式

@Configuration
class Beans {
    @Bean
    @Profile("dev")
    public SomeType devSomeType() {
        return new SomeType("in dev");
    }
    @Bean
    @Profile("prod")
    public SomeType prodSomeType(){
        return new SomeType("in prod");
    }
}

与之相关的InitializingBean接口(唯一方法void afterPropertiesSet()),实现了该接口的@ConfigurationProperties配置类会在spring根据配置设置完字段值后被调用接口方法。

@ConfigurationProperties配置类的字段被标记了@NotNull时不允许配置环境中缺失该配置键。

@Component('bean-name') <--> context.getBean('name')

@Bean("name", initMethod="init")定义bean和初始化方法,在构建完对象实例后需要调用initMethod指定的初始化方法。

@Bean, @Component, @Service, @Repository都是定义bean,只是表达应用场景不同,标记type时使用type名作为bean名字。

@Bean结合@Scope('singlton'|'prototype'),定义bean为单例或者非单例。

@Service 提供interface的实现。 注解到interfac的implementation上(interface无需其他注解),使用对应接口时,在字段上标注@Autowried,spring提供对应实现。

@Bean on method, 默认使用方法名作为bean名字。

@Bean(name="beanId") ; @Autowired @Qualifier("beanId")
在字段上使用@Qualifier时尤其注意同时需要@Autowired,@Qualifier不蕴含@Autowired,如果上文中没用@Autowired语境(@Bean定义蕴含@Autowired、自动调用的构造函数需要标注@Autowired也就蕴含@Autowired等),则需额外加上@Autowired。

spring相关注解的参数是否可以是SPEL运行时值,如@Qualifier("${beanNameFromConfigFile}")。 不能。就@Qualifier而言,其参数被直接认为是bean名字字面量。

@Configuration (for class),标记包含了若干bean定义的类。类中一般用@Bean标记方法以定义一个bean。结合@ComponentScan('package-to-scan'),定义需要扫描的包。

@Configuration用于bean定义容器,不要与@ConfigurationProperties同时使用。

@PropertySource('resource-path'),指定接下来的值搜索资源,仅支持.property文件,不支持.yml。(如@Value查找的配置资源)。

for field/parameters
@Value('value literal')
@Value("\({conf-key-name}") 配置值 @Value("\){placeholder}")这种形式在配置未定义时不返回null,而是抛出异常,要获得未配置时返回null的功能通过提供默认值实现@Value("\({key:#{null}}") @Value("\){key:defaultVal}") 配置值,提供默认值
@Value("SpEL-expression") 其他SpEL表达式
expression中可以使用${key}获取property
@Value("#{systemProperties['os.name']}") 系统属性(获取系统属性另可考虑@Autowired Environment env;)
@Value("#{T(java.lang.Math).random()*10}") 带java表达式的值
@Value("#{someBean.someField}") 对象字段值
@Value("resource-path") for type Resource/String/InputStream,对String类型注解时会读取资源内容;注入到InputStream/Stream时,如果文件资源不存在会导致异常,注入Resource时可根据Resource.exists()判断资源是否存在。

@Value('http://xxx'|'classpath:'|'file:')

@Value读取配置值时是否可以指定配置文件???

@Bean(initMethod='', destryMethod='')

@PostConstruct 注解方法为构造器后调用的初始化方法(init-method)

@PreDestroy destroy方法,bean销毁之前调用(destroy-method)

@Import(X.class) 将类注入spring容器

可以注入Set,List
提供默认值null:@Value("\({key:#{null}}"); 提供默认集合:@Value("\){key:#{{'a','b'}}}"),注意嵌套的花括号,#{}表示其中的内容是SpEL,内嵌的花括号{}表示这是一个内联字面量集合/列表。(拉倒吧,根本没法成功为@Value标记的集合注入值,测试环境:spring boot 2.0, .yml, .properties文件)

yaml中配置对于@Value注入的集合值!!!无法成功(spring-boot版本2.0, spring 5.0.6)!!!,无论值格式配为[x,y]、x,y、 - x<换行> - y的形式均无法成功。如配置为“x,y”(不含引号)形式时,得到的是一个只包含一个元素的List/Set,该元素是字符串“x,y”;配置为“[x,y]”(不含引号)形式时使用了默认值(事实上以值以方括号开始时甚至不能被注入为String);配置为“- x<换行> - y”时也会使用默认值。

.properties定义也如此,不能以逗号拼接的串注入集合(.properties定义列表的格式 key[0]=val1<换行>key[1]=val2)

逗号拼接串可注入数组类型(String[], Double[]之类)。

注入List只在@ConfigurationProperties标注的类的字段情况下才能成功,支持灵活的配置形式,包括[x,y]、x,y、 - x - y。(需提供字段setter)

@Value("${key:#{null}}") //默认值null
Set<String> p;

@Value("${key:#{{'a','b'}}}") //默认值{a,b}
Set<String> p;

@ConfigurationProperties类字段类型:
Map<String,Object>会将属性依据点号.解析为嵌套结构。
支持Map<String,Map<String, *>嵌套类型。
不想捕捉嵌套Map结构,想保留带点号的键?声明为Map<String,String>,或类型Properties,该类型将保留点号的属性为键。

如果需要spring容器相关资源,那将bean类声明继承 XXXAware(BeanNameAware, ResourceLoaderAware等)接口,实现相关setXXX方法,由spring容器调用,资源作为实参提供。

并发、Executor、多线程、异步方法:
@EnableAsync for class, @Async for class/method, implements AsyncConfigurer, TaskExecutor, ThreadPoolTaskExecutor.
方法异步执行行为定义在一个非final的public方法上,通过标注@Async完成,spring通过继承方法所在类以及用Executor异步执行目标方法来实现。目标方法应返回void或者Future,返回Future情况应用在需要取得异步执行结果的或控制异步执行的场景,在方法定义中,通过用new AsyncResult<>(...)(该类继承自Future)包装方法执行结果数据以返回Future。

@Component
public class MyAsyncTask {
    @Async
    public void doItAsyncIgnoreResult() {   // 不关心返回结果
        System.out.println("done");
    }
    
    @Async
    public Future<Double> doHeavyComputation(double s) {
        //利用输入异步进行复杂运算,返回一个结果
        double r = 0;
        ...     //复杂计算
        return new AsyncResult<>(r);
    }
}

注意:对异步方法的类的字段的读取,不能直接通过.<field>获取,一定需要通过方法获取(getter),否则无法获取到值,这是因为spring注入了生成代理子类后多态问题导致。

计划任务:
@Service for class, then @Scheduled(fixRate= | cron='unix cron format') for method; @EnableScheduling;自动被初始化和调用。

@Scheduled中时间配置可以是单位为ms的long类型,也可配字符串,字符串可以为spring配置注入表达式("${xx.xx}")。时间字符串格式可以是数值(同long类型参数),也可以是Duration格式(注意带前缀“P”以及时间前缀T,如1天6小时"P1dT6h",6小时"PT6h")。

@Service
class Svc {
    @Scheduled(fixRate=5000)
    public void f(){}
    @Scheduled(fixDelayString="${s.rate}")
    public void g(){}
}
@Configuration
@ComponentScan('')
@EnableScheduling
class Cnf{}

public class Main{
    public static void main(String[]args){
        SpringApplication.run(Main.class,args)
    }
}

可以声明被其他注解标注的注解(@interface),标注在注解上的注解称为元注解,被标注的注解称组合注解(composite annotation),组合注解具备所有元注解功能。组合注解的参数覆盖元注解的参数。可以简单理解为一组元注解的别名。

获取配置值的容器Environment,已被定义为bean(能被@Autowired)。

定义bean的销毁顺序?比如某些业务bean在销毁时需要进行数据库操作,此时要求数据库连接bean在业务bean之后关闭。 <=== spring创建bean本身记录了依赖关系,销毁时按创建时的依赖顺序反序销毁。

spring程序启动时排除扫描特定类:

@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = ASSIGNABLE_TYPE, value = {MyUnneccessaryConfig.class})})

Resource接口表示IO输入资源,可通过@Value("")自动注入,接口下有方法.getFile,只有资源在文件系统直接能够获取时才能调用该方法。在自动注入类路径资源时需注意,对于一个项目类路径中的资源,.getFile在项目打jar包前能正常返回,但在jar包后将抛异常(因打入jar后资源将不能直接通过文件系统获取)。

Type Conversions 数据类型转换:

import org.springframework.core.convert.converter.Converter;

@Component
public class MyConverter Converter<S,T> {
    @Override
    public T convert(S ){
    }
}

在某些上下文中可以指定额外的注解来提供参数,如时间模式参数注解与Web参数解析注解结合使用@RequestParam @DateTimeFormat(pattern = "yyyyMMddHH")

SpEL

SpEL:spring expression language.

Spring Data

类型安全的排序:

TypedSort<Person> person = Sort.sort(Person.class);

TypedSort<Person> sort = person.by(Person::getFirstname).ascending()
  .and(person.by(Person::getLastname).descending());

@TypeAlias:在将java bean转为数据库(主要是非关系型)记录时框架会增加一个表示java类型的字段_class其一般为类的全限定名,可用注解@TypeAlias(value:String)标注类型以使得其在映射时使用注解中提供的名字,该功能可使得Java类型被重构(重命名)后数据库中记录仍可正常被映射。

Spring Data - JPA

spring boot data dependecy -> artifact: spring-boot-starter-jdbc

Spring Data是一个用于简化数据库访问,并支持云服务的开源框架。其主要目标是使得对数据的访问变得方便快捷,并支持map-reduce框架和云计算数据服务。 Spring Data 包含多个子项目:

  • Commons - 提供共享的基础框架,适合各个子项目使用,支持跨数据库持久化
  • JPA - 简化创建 JPA 数据访问层和跨存储的持久层功能
  • Hadoop - 基于 Spring 的 Hadoop 作业配置和一个 POJO 编程模型的 MapReduce 作业
  • Key-Value - 集成了 Redis 和 Riak ,提供多个常用场景下的简单封装
  • Document - 集成文档数据库:CouchDB 和 MongoDB 并提供基本的配置映射和资料库支持
  • Graph - 集成 Neo4j 提供强大的基于 POJO 的编程模型
  • Graph Roo AddOn - Roo support for Neo4j
  • JDBC Extensions - 支持 Oracle RAD、高级队列和高级数据类型
  • Mapping - 基于 Grails 的提供对象映射框架,支持不同的数据库
  • Examples - 示例程序、文档和图数据库
  • Guidance - 高级文档

spring jpa接口中的实体字段名几乎都是指ORM映射之后的类字段名,如repo中的方法名关乎的字段名、Sort相关用到的字段名。

spring jpa中把提供数据库CRUD的interface称为Repository,可以定义继承自JpaRepository<T,ID>的interface,类型参数中的T是数据库表实体类,ID是主键类型,Repo标注上@Repository,spring jpa将自动生成继承自SimpleJpaRepository的代理实现,Repo接口中方法的名字定义功能(如果在方法上无@Query等注解),方法名与功能实现的对应规则如下

  • findOneByXXX, findAllByXXX查询数据;existsByXXX存在性检查;deleteByXXX删除;countByXXX计数;
  • findAllByXxxAndYyy(XXXType xxx, YYYType yyy, Pageable)定义通过字段Xxx和Yyy查询数据,字段对应类型为XXXType和YYYType,Pageable是分页查询参数,返回对象页数据Page<>,字段Xxx是jpa java实体类的字段名,按camel case规则定义大小写转换方法,另可定义不带Pageable的该方法,功能为根据字段查询所有满足数据,返回List。
  • findOneByXxx,根据唯一性字段(字段组合)查询数据,返回Optional<>。
  • findAllByXxxContaining(String xxx),字符串型字段包含(部分匹配)查询
  • findAllByXxxContainingIgnorcase(String xxx),不区分大小写查询。
  • deleteByXxx,根据字段删除,需要标注@Transactional。
  • updateXXXByXXX,需要标注@Transactional。

可以不通过方法名定义功能,使用自定义查询。通过在接口方法上标注@Query("JPA语句"),语句可以是查询、更新、删除语句,更新、删除语句需要@Transactional支持(方法上标注该注解),更新语句还必须标注@Modifying表明操作将修改数据。语句中涉及的数据字段名是jpa实体类的字段名,不是SQL表中的字段名。在语句中引用接口方法参数的方式有两种:一种是通过参数顺序引用,?<数字>引用数字对应第n个参数(从1开始),如?1引用第一个参数;另一种是通过绑定的名字引用,在方法参数列表中对要绑定的参数标注@Param("name"),标记为名字"name",在@Query()中通过:<名字>引用,如利用“:name”引用通过@Param标记的名为“name”的参数。

@Modifying标注一个SQL INSERT, UPDATE, DELETE, 甚至DDL语句,@Modifying标注的方法的返回类型只能是voidint/Integer,整型返回值表示受影响的记录条数。

@Query中使用的查询语言叫 JPQL

@Query 删除:
delete from BeanClass varName where ...

可否直接引用java方法参数名(未通过@Param定义绑定名)? <=== 不可以,方法参数名在运行时本身已不存在。

可否以非基本类型(自定义类)作为参数,在@Query中引用参数的字段(或字段getter)? <=== 利用名字绑定+SpEL,如@Query("... where id=:#{#u.uid}") int count(@Param("u") MyUser my)。

@Query中使用SpEL:@Query中引用参数时通过 ?#:#触发SpEL方式引用机制。利用?#或:#后紧随的花括号裹挟SpEL表达式。

@Query中不能简单通过":x.prop"来访问绑定的复杂类型参数"x"的属性"prop",可通过SpEL来访问。

findAll返回List<>类型,分页的findAll(有Pageable参数)返回Page<>类型,findOne返回Optinal<>。自定义的findAllByXXX可定义返回List<>或流类型Stream<>,Jpa父类的findAll(无参)返回List<?>,如果想添加一个返回Stream<>功能,需要额外加方法(如Stream<> streamAll()),同时添加注解@Query("select t from EntityClass t")(因为已有List<> findAll(),且不能通过返回类型重载方法)。

使用返回类型为Stream<>的Jpa方法时,其调用函数(caller method)需要标注@Transactional(readonly=true),在Jpa方法上标注无用。(所以对于调用函数上不能有注解,或者调用函数中有多次调用Jpa方法而@Transactional应该只指一次事务的情况怎么办呢?)

JpaRepository中的save()(以及saveXXX())用以向数据库表中插入数据,如果存在相同数据(违反唯一性约束),将抛异常。

.save()在记录已存在时将抛异常,那如何做insert if not exists? <=== 利用@Transactional和Repo写代码级的事务。(Jpa不允许自写insert语句)

@Transactional可标注在spring管理的class的方法上(不论是自己实现的Jpa Repo class还是非Repo class,该方法利用Jpa Repo实现事务),以实现一个数据库事务。

@Transient 此字段不做持久化(必须放在字段上,不能getter)

@Field('name')定义数据库中的字段名(默认为java类字段名)
@javax.persistence.Column(name=, columnDefinition="int(4) default 0") 定义数据库中的DDL(默认值)

定义数据库字段默认值:
@ColumnDefault(String[] value)

@PrePersist annotate on a method

@Id会将该字段映射到_id,因此@Field("name")不起作用。mongorepo中findbyid始终在_id上查找。

在@Configuration类上,标注@EnableJpaRepositories@EntityScan,前者定义扫描Jpa Repository类(包)及相关配置,后者定义扫描的Jpa实体类(包)。
@EnableJpaRepositories(basePackageClasses = SomeoneJpaRepo.class(定义扫描的包),entityManagerFactoryRef="beanName",transactionManagerRef="beanName"),entityManagerFactoryRef和transactionManagerRef用于自定义EntityManager和TransactionManager,自定义DataSource的bean需要用到这两个配置。
@EntityScan(basePackageClasses = SomeoneJpaEntity.class(定义扫描的包)
Jpa实体类需标注@Entity,并通过@Table(name="",indexes={@Index})映射表信息(表名、索引、数据约束等)。(对应SQL数据表定义应提前在数据库中完成,Jpa实体类中的字段名、索引等定义只用于支持框架jdbc查询,不用于创建数据库。)

@Index(columnList="myField ASC, myField2"),这里是字段名是java属性名而非数据库表中的字段名,可指定升降序,可指定多个列。

时常需要配置或自定义RMDBS连接池管理类,尤其多数据源管理场景,通过自定义EntityManager、TransactionManager实现。

PageRequest.OrderBy 与带下划线的类字段;Repo中带下划线的方法

@OneToOne @OneToMany @ManyToOne @ManyToMany

联合表中找不到数据情况:@NotFound(IGNORE|Exception)

关于实体类A的仓库Repo<A, Id>中定义返回其他类别,如B,的接口public B findX(),通过用@Component标注从A到B的类型转换服务org.springframework.core.convert.converter.Converter<A,B>不能自动转换成功,会报错"no converter found capable of converting from type ...",需使用@javax.persistence.Converter(autoApply=true),或在接口方法上定义@Query('select new B(...)'),或使用Projection机制。

枚举映射

用注解@Enumerated标注字段可将枚举类型映射到数据库,值映射方式有2种,整型或字符串,分别使用java enum的originalname属性。这两种方式所提供的数据正确性严重依赖enum的源码的结构性质(而非代码逻辑),重排序或重命名enum实例都将导致数据库中原来的不正确,这些方式并不安全。在JPA 2.1之后,可利用AttributeConverter来自定义枚举数值映射。

@Entity
@Table(name="table_name")
public class Person implements Serializable {
    @Convert(converter = PassTypeDBValueConverter.class)
	protected PersonType persontType;
}

@Converter(autoApply = true)
public class PersonTypeDBValueConverter implements AttributeConverter<PersonType,Integer> {
	@Override
	public Integer convertToDatabaseColumn(PersonType attribute) {
		if(attribute==null)return null;
		return attribute.getCode();
	}

	@Override
	public PersonType convertToEntityAttribute(Integer dbData) {
		if(dbData==null) return null;
		return PersonType.fromCode(dbData);
	}
}

public enum PersonType {
	Unknown(0);

	private int code;
	private PersonType(int code) {
		this.code = code;
	}

	public int getCode() {
		return code;
	}
	private static  Map<Integer, PersonType> code2enum=new HashMap<>();
	static {
		PersonType[] vs = values();
		for (int i = 0; i < vs.length; i++) {
			code2enum.put(vs[i].code, vs[i]);
		}
	}

	public static PersonType fromCode(int code) {
		PersonType v=code2enum.get(code);
		if(v!=null) return v;
		else throw new IllegalArgumentException("Not found such code");
	}
}

示例

//spring @Configuration类
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackageClasses = UserEntityTrackingService.class, entityManagerFactoryRef = "el.tracking.entityManagerFactory", transactionManagerRef = "el.tracking.transactionManager")
@EntityScan(basePackageClasses = UserEntityTracking.class)
public class MyJpaConfig {
    @Bean("el.tracking.datasourceRef")
	@ConfigurationProperties(prefix = "el.tracking.datasource")
	public DataSource elTrackingDataSource() {
		return DruidDataSourceBuilder.create().build();
	}

	@Bean("el.tracking.entityManagerFactory")
	public LocalContainerEntityManagerFactoryBean elTrackingEntityManagerFactory(@Qualifier("el.tracking.datasourceRef") DataSource elTrackingDataSource,
																				 EntityManagerFactoryBuilder builder,
																				 JpaProperties jpaProperties) {
		return createEntityManagerFactoryBean(elTrackingDataSource, builder, jpaProperties);
	}
	
	@Bean("el.tracking.transactionManager")
	public PlatformTransactionManager elTrackingTransactionManager(@Qualifier("el.tracking.entityManagerFactory") EntityManagerFactory elEntityManagerFactory) {
		return new JpaTransactionManager(elEntityManagerFactory);
	}
	
	private static LocalContainerEntityManagerFactoryBean createEntityManagerFactoryBean(DataSource dataSource, EntityManagerFactoryBuilder entityManagerFactoryBuilder, JpaProperties jpaProperties) {
	return entityManagerFactoryBuilder
			.dataSource(dataSource)
			.properties(jpaProperties.getHibernateProperties(new HibernateSettings()))
			.packages(UserEntityTracking.class) //设置实体类所在位置
			.persistenceUnit("defaultPersistenceUnit")	//任意名字
			.build();
	}
}
//jpa实体类
@Entity
@Table(name = TableName,
		indexes = {@Index(columnList = UserId), @Index(columnList = UserId + "," + EntityId, unique = true)})
public class UserEntityTracking {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)    // for mysql, IDENTITY is ok,AUTO will not, while, the latter is the default
			Long id;
	// uk: (userId+entityId)
	@Column(name = UserId, nullable = false)
	String userId;
	@Column(name = EntityId, nullable = false)
	String entityId;
	@Column(name = EntityName, nullable = false)
	String entityName;
	// getter/setter's follow here
}
//数据库表名、字段名常量
public interface UserEntityTrack {
	String TableName = "user_entity_track";
	String UserId = "user_id";
	String EntityId = "entity_id";
	String EntityName = "entity_name";
}
//Repo类
@Repository
public interface UserEntityTrackingService extends JpaRepository<UserEntityTracking, Long> {
    long countByUserId(String userId);
	boolean existsByUserIdAndEntityId(String userId, String entityId);
	Page<UserEntityTracking> findAllByUserId(String userId, Pageable pageable);
	List<UserEntityTracking> findAllByUserId(String userId);
	Optional<UserEntityTracking> findOneByUserIdAndEntityId(String userId, String entityId);
	Page<UserEntityTracking> findAllByUserIdAndEntityNameContainingIgnoreCase(String userId, String entityName, Pageable pageable);

    //@Query("select t.entityName from UserEntityTracking t where t.userId=?1 and t.entityId=?2")
    //List<UserEntityTracking> myFindAll(String userId, String entityId);
	
	@Transactional  // <-- Transactional
	void deleteByUserIdAndEntityId(String userId, String entityId);

    @Transactional  // <--
	@Modifying  // <--
	// maybe a better method name
	@Query("update UserEntityTracking t set t.entityName=?3 where t.userId=?1 and t.entityId=?2")
	void myUpdateName(String userId, String entityId, String entityName);
	// bound parameters
	// 	@Query("update UserEntityTracking t set t.entityName=:name where t.userId=:uid and t.entityId=:eid")
	// void myUpdateName(@Param("uid")String userId, @Param("eid")String entityId, @Param("name")String entityName)
	
	@Query("select t from UserEntityTracking t")
	Stream<UserEntityTracking> streamAll(); // 不能定义Stream<> findAll();因为父类已有方法List<> findAll();
	
	//
	@Query
}


//数据库事务
//或者在自己实现的Repo class
@Repository
public MyRepoImpl {
    @Autowired
    EntityManager entityManager;
    @Transactional
    public boolean insertIfNotExisting(String mydata) {
        if(entityManager.exists(...)) return false;
        else {
            entityManager.persist(...);
            return true;
        }
    }
}
//或者在任意spring管理的class
@Service
public class MyService {
    @Autowired
    Repo myrepo;
    @Transactional
    public boolean insertIfNotExisting(String mydata) {
        if(myrepo.exists(...)) return false;
        else {
            myrepo.save(...);
            return true;
        }
    }
}

Spring Data - H2

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:h2:file:~/.h2/testdb      #以文件为存储系统
    #jdbc:h2:mem:testdb  #以内存为存储系统
    username: test      #定义
    password: 1234
    driver-class-name: org.h2.Driver
    platform: h2
  jpa:
    database: H2

spring.h2.console.enabled: true

spring.jpa.database-platform: org.hibernate.dialect.H2Dialect

H2控制台地址:/h2-console/ (有输出在日志),url,username,password使用spring配置文件中定义的数据。

Spring Data - Elasticsearch

spring data elasticsearch

maven依赖:org.springframework.boot:spring-boot-starter-data-elasticsearch

@Configuration
class Config {

  @Bean
  RestHighLevelClient client() {

    ClientConfiguration clientConfiguration = ClientConfiguration.builder() 
      .connectedTo("localhost:9200", "localhost:9201")
      //.withConnectTimeout(Duration.ofSeconds(5))
      //.withSocketTimeout(Duration.ofSeconds(3))
      //.useSsl()                             
      //.withDefaultHeaders(defaultHeaders)
      //.withBasicAuth(username, password) 
      . // ... other options
      .build();

    return RestClients.create(clientConfiguration).rest();                  
  }
}

日志级别配置相关类:org.springframework.data.elasticsearch.client.WIRE 。

对象映射,在两种实现中选择,他们的抽象是EntityMapper

  • Jackson Object Mapping
  • Meta Model Object Mapping

Jackson Object Mapping:

// Jackson2 Object Mapping Configuration
// 	AbstractElasticsearchConfiguration already defines a Jackson2 based entityMapper via ElasticsearchConfigurationSupport.
@Configuration
public class Config extends AbstractElasticsearchConfiguration { 

  @Override
  public RestHighLevelClient elasticsearchClient() {
    return RestClients.create(ClientConfiguration.create("localhost:9200")).rest();
  }
}

注意:
CustomConversions, @ReadingConverter & @WritingConverter cannot be applied when using the Jackson based EntityMapper.
Setting the name of a mapped field with @Field(name="custom-name") also cannot be used with this Mapper。

Meta Model Object Mapping:

@Configuration
//@EnableElasticsearchRepositories
public class Config extends AbstractElasticsearchConfiguration {

  @Override
  public RestHighLevelClient elasticsearchClient() {
    return RestClients.create(ClientConfiguration.create("localhost:9200")).rest()
  }

  @Bean
  @Override
  public EntityMapper entityMapper() {                                 

    ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(
      elasticsearchMappingContext(), new DefaultConversionService()    
    );
    entityMapper.setConversions(elasticsearchCustomConversions());     

  	return entityMapper;
  }
}

ElasticsearchEntityMapper可利用模型类中元信息进行Java对象到ES文档的映射,其中可用的注解如下:

  • @Id 于字段。
  • @Document 标注到类上,表明需要映射。部分参数:
    • indexName
  • @Transient 于字段,表明该字段不需要被映射(默认全部私有字段都会被映射)。
  • @PersistenceConstructor 于某个构造器,表示框架创建实例时调用的构造器,构造参数的名字将对应数据库文档的字段。访问权限可窄至包权限。
  • @Field 于字段。
  • name 数据库中字段名。
  • type
  • format, pattern 时间格式。
  • store 默认false
  • analyzer, searchAnalyzer, normalize
  • copy_to

@ReadingConverter @WritingConverter 自定义读写时的数据类型转换器。

ElasticsearchOperations中的有关类的类信息(Class<T>)参数,如方法queryForObject( , Class<T>), queryForObject( , Class<T>)等中的Class<T>参数,均指持久实体类(标注@Document的类)的类信息,非持久类则会导致错误,如nested,object字段对应的元素类。

inner_hits

禁用ElasticsearchRestHealthIndicator

management.health.elasticsearch.enabled: false

Spring Data - MongoDB

<!-- dependency in pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring‐boot‐starter‐data‐mongodb</artifactId>
</dependency>
@Document(collection="my_coll")
public class EntityBean {
    @Id
    String _id;
    
    String myfield; // field name in db is the same as the java field name if no annotation @Field
    @Field("custom-name-in-db")
    String myname;
}

public interface MyRepo extends MongoRepository<EntityBean, String> {
}
# db connection configuration in application.yml
spring.data.mongodb:
  host: 
  database:

Spring Security

spring:
  security:
    username: <默认用户名>
    password: <默认用户密码>

若未配置默认用户名则是admin,若未配置默认用户的密码则将生成随机密码并在控制台打印出。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth
				.inMemoryAuthentication()
				.withUser("")
				.password(passwordEncoder().encode("")) //必须使用encoder编码密码
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
				.authorizeRequests()
				.antMatchers("/login/**").permitAll()
				.anyRequest().authenticated()

				.and()
				.formLogin()

				.and()
				.httpBasic()

				.and()
				.csrf().disable();
	}
	
	public PasswordEncoder passwordEncoder() {
	    return new ...;
	}
}

//配置特定profile时禁用security
@Configuration
@Profile("test")
@Order(1)       //需要定义@Order,若程序中同时定义了其他WebSecurityConfigurerAdapter
public class NoWebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/**");
	}
}

Spring Boot

对于spring boot的maven项目,可使用属性java.version来设置JDK版本。

<properties>
    <java.version>1.8</java.version>
</properties>

spring boot引入自动配置机制,根据条件自动定义某些bean,其触发条件及bean定义过程定义在某个类中,一般命名为XxxAutoConfiguration,然后在META-INF/spring.factories文件中配置该为一种spring-boot的自动配置类,spring将会扫描该文件读取该类,根据条件决定是否生成其中定义的bean。

在应用程序不需要某些自动配置类时,需要排除这种自动配置(比如程序无SQL库时,我们就不需要spring-jpa依赖包中的DataSourceAutoConfiguration,不排除自动配置类的话其将读取配置连接数据,但会连接失败导致程序异常),可在程序入口用注解编码式地排除@EnableAutoConfiguration(exclude=),或@SpringBootAppliction(exclude=)。也可通过配置文件排除spring.autoconfigure.exclude: <class-name>

不建议自动注入类字段:类字段声明同时初始化时不得使用自动注入的类字段,因为声明初始化时标记为自动注入的类字段实际还未被注入,应将声明初始化分离初始化到构造函数。

@ConditionalOnMissingBean,对于没有提供参数时,且与@Bean(name="xx")共同出现,其缺失Bean的条件是仅针对类型还是会针对具有名字的bean(即条件中不给name参数时是否会推测为@Bean中name的参数<不会)。<=条件中要 给出名字,否则认为是类型。如:

public class Conf {
    @Bean
    public String someStr(){
        return "no-condition";
    }

    //这里的缺失判断条件是 (1)缺失类型为String的任意Bean? 还是(2)名为"group"的String类型的Bean?  <==条件被认为是类型,不会推测@Bean中的名字。
    //下面不会被执行
    @ConditionalOnMissingBean 
    @Bean(name="group")
    public String defineGroup(){
        return "my-group";
    }
}

spring boot程序可以使用程序参数覆盖配置文件中的配置。(java -jar xxx.jar --someKey=someVal,参数需在-jar后,也就说那是程序参数并非jvm参数)

artifact org.spring*.boot:autoconfigure中有@ConditionalOnXXX(OnBean存在bean, Class存在类, MissingClass缺失类, Property存在配置等)的组合注解。相关文件:META-INF/spring.factories。

idea -> spring initializer-> maven project-> (web, ...) ->...

@ConfigurationProperties

@ConfigurationProperties(prefix="a.b.x") for class/method
这里的property应理解为java类的property:field+getter+setter,注解功能是指根据类字段名字找到配置文件中对应key的值来自动注入到字段,找key时使用@ConfigurationProperties中的prefix指定的字符串作为前缀。

所有getXXX(无论何种访问权限)的方法默认都被认为有对应绑定配置(尤其对于返回非基本类型的getXXX方法,会认为有绑定的内嵌属性,如果实际配置中没有,则spring报错)。

设定@ConfiguraionProperties标记类的字段默认值??以字段初始化方式实现。

@ConfigurationProperties标记的类中不要使用@Value标记字段。

@ConfigurationProperties支持嵌套类,嵌套类字段对应的配置名是下级结构。

@ConfigurationProperties("a")
class A {
    int num;    // a.num
    Host host;  //
}
static Host {
    String host;    // a.host
    int port;       // a.port
}

能否用@ConfigurationProperties标记interface,如JPA中的自声明的Repo,那么该Repo在被自动提供实现时相关的被标记@ConfigurationProperties的属性会使用自声interface上的@ConfigurationProperties的prefix吗? <=== 不能。(jpa repo中的数据源配置前缀的指定需要在定义数据源bean时指定)

@ConfigProperties和@Value被设计为无关联的,也就说@ConfigProperties中的prefix对@Value标记的字段值的查找不起作用。

@ConfigurationProperties还可标记到方法上,此时,简单看来,该方法中(直接或间接用到的)被标记了@ConfigurationProperties的class的前缀将被方法上@ConfigurationProperties中的prefix参数覆盖。
具体地,@ConfigurationProperties标记的方法(该标记中的prefix姑且称为此方法中的上下文配置前缀)在运行时返回的对象的实际类型必须是被标记了@ConfigurationProperties的类的实例(同时也是编译期/源代码声明类型或子类),之后spring boot会根据方法的上下文配置前缀及配置类字段名读取spring环境配置值,为返回的实例对象设置字段值,仅设置对应的配置键已存于上下文配置环境的字段(也就说对于对应配置缺失的java类字段,其初始化值或者方法返回实例前被设置的值都可以生效,而不是被spring设为等效0/null,可以达到设置@ConfigurationProperties配置类字段默认值的目的)。

@Configuration
public class C {
    @Bean("anothorDatasource")
    @ConfigurationProperties("druid.second")
    // 更改了DruidDataSource默认的配置前缀
    public DataSource secondData() {
        		return DruidDataSourceBuilder.create().build();
    }
}

//TODO @ConfiguratioProperties标注到返回POJO的方法上,POJO类没有任何注解,也意味着没有@ConfigurationProperties及@Value等spring boot注解。

@SpringBootApplication on class of main entry of spring boot application

@Autowired 可标注在一个被spring管理的bean的普通方法上,使得spring在初始化完应用后自动调用。比如可标注到以配置为目的、返回void的方法上。

如果用gradle,可能会遇到问题,下载包,相当慢,没发现下载到本地maven repo也不知道下载到哪儿,手动用mvn下载:
org.springframework.boot:spring-boot-dependencies:1.5.2.RELEASE:pom
org.springframework.boot:spring-boot-loader-tools:1.5.2.RELEASE
io.spring.gradle:dependency-management-plugin:1.0.0.RELEASE
……

使用spring-boot-data-jpa,需要在应用启动器上标注@EnableJpaRepositories(basePackageClasses = XXRepo.class),为了实体管理器管理实体类(表、索引相关),需要注册实体类,通过@EntityScan(basePackageClasses = XX.class)实现,否则报错Not a managed type。

表在程序执行前应存在于数据库中。

关于主键的异常:com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'portal.hibernate_sequence' doesn't exist,主键生成策略更改为@GeneratedValue(strategy = GenerationType.IDENTITY)可解决问题。

spring boot 引入多个properties/yml文件???

@Configuration类里不能@Autowired ConversionService。

暴露关闭程序应用的接口(优雅停机),引入依赖org.springframework.boot:spring-boot-starter-actuator,在程序配置中写入

# spring boot 2.0以前的版本的配置键不一样
# 默认仅info,health,不含shtudown,因此需显式引入
management.endpoints.web.exposure.include = info, health, shutdown
# 默认未开启,显式开启
management.endpoint.shutdown.enabled = true

对actuator管理端口下的/shutdown地址发送http POST请求,无需请求参数,即可提交关闭应用的请求,会收到一条跟请求者说再见的http响应消息。

关闭程序启动时的"spring boot"字样的条幅:配置中加入 spring.main.banner-mode: off,若在.yml文件中则需要对off加引号,及spring.main.banner-mode: 'off',以阻止off被yml解析器自动转为布尔值。

maven依赖管理、打包插件pom配置(请注意其中注释提示):

<!--pom.xml-->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring.boot.version}</version><!--2.0.0.RELEASE-->
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--
        该配置管理spring-boot相关依赖的版本时很方便,但一定注意因此引发覆盖其他地方定义的依赖的版本,如将org.elasticsearch.client:transport:6.3.0的依赖定义顺序置于spring-boot之前,项目仍会使用spring-boot:2.0.0中es:5.6.8的版本。
        -->
    </dependencies>
</dependencyManagement>

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <version>${spring.boot.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
            </goals>
        </execution>
    </executions>
</plugin>

org.springframework.boot:spring-boot-maven-plugin 该打包插件打出的包(这里称为项目jar包)结构不同于一般的jar包(主要是对依赖包、项目class的存放),它通过将项目所有依赖的jar包打到/BOOT-INF/libs/目录,而且仍以jar存放,没有全部解压出依赖jar包中内容(包括层级目录和.class文件)放到项目包根目录,项目的所有.class全部放在/BOOT-INF/classes/目录中,项目jar包的根目录下放的是spring boot launcher包(由插件自己引入)的类。项目包根目录下的文件目录结构(JVM能直接读到classpath的目录结构),跟通常打出的包比较来看,有点类似项目只spring boot launcher包,spring boot应用启动时,launcher在运行时自己去加载/BOOT-INF下的jar和.class,使得运行时项目class及其依赖class对jvm可见。

spring-boot-maven-plugin将会把所有需要的依赖都打入程序包,包括scope为provided的。

这种打包方式对storm项目不可行,storm nimbus在创建storm组件(spout, bout)实例后的分发supervisor过程中会因找不到项目class及其依赖class而导致分发失败。
原因(待重新梳理验证):【将此jar包添加到classpath后jvm无法感知项目自身class和依赖的class,因为spring boot launcher还未被执行,classpath中还没有项目class和依赖class的信息】
同时,项目main入口类的getClass.getClassLoader的工作目录成了项目jar包下的/BOOT-INF。

Spring boot(2.0, spring 5.0)未提供String转java.time.Duration的内置转换器,因此同时也无法@Value注入Duration值。

打包插件的layout配置: TODO

  • ?(默认)
  • ZIP/DIR
  • WAR

程序启动时指定主类的方式:

java -cp x.jar -Dloader.main=my.MainClass org.springframework.boot.loader.PropertiesLauncher

支持其他格式配置源:TODO

//监听应用初始化,向spring环境中添加自己的属性解析器(配置源)
public class MyAppCtxInit implements ApplicationContextInitializer<ConfigurableApplicationContext> {

	@Override
	public void initialize(@NonNull ConfigurableApplicationContext configurableApplicationContext) {
		configurableApplicationContext
		    .getEnvironment()
		    .getPropertySources()
			.addLast(new MyPropertySource());
			// .addLast添加在列表尾, .addFirst添加在列表头, .addBefore .addAfter添加在其他解析器的前后位置
	}
}
//自己的属性解析器,至少实现方法Object getProperty(String name)
//MyConfigClass类型参是自己的代理配置类型(比如com.typesafe.config.Config以支持解析.conf配置文件)
class MyPropertySource  extends PropertySource<MyConfigClass> {
    ...
    @Override
	public Object getProperty(@NonNull String name) {
		return ...; //
	}
}

文件 META-INF/spring.factories :

org.springframework.context.ApplicationContextInitializer=MyAppCtxInit

外部配置

程序默认加载文件名为application的系列配置,如application.properties,application.yml等配置文件,这个文件名可以配置为其他,通过spring.config.name,这个属性加载得较早,应该通过JVM系统属性、操作系统环境变量等方式配置。
配置文件默认在类路径(classpath)下寻找,可通过指定spring.config.location来指定加载配置文件的若干路径,以逗号隔开,可以是文件或目录,若目录则必须以/结尾,可添加文件系统的路径(file:...)或类路径中的路径(classpath:...)。

metrics:

<!--pom.xml-->
<!--添加依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>

配置暴露出metric相关的web接口:

#application.properties
management.endpoints.web.exposure.include= metrics,prometheus

自动配置 auto-configure

spring可自动加载定义了bean的配置类,这就像项目内标注了@Configuration的普通配置类,但后者需要在应用启动类上的注解@SpringBootApplication中的参数里指明该普通配置类的位置(如果在同个包下则可不显式指明),而自动配置特性使得自动配置类只要存在于类路径中就被认为需要进行自动配置(除非显式地被排除),且自动配置类一般都是条件性地定义bean(而不像普通配置类那样无条件直接定义)。自动配置类一般独立成包,不像普通配置类与业务程序同项目。

自动配置类的一般使用场景是,在达到某种条件时自动定义某些bean。、

设计实现自动配置类的要点:

  • 配置类标注@Configuration(或@SpringBootConfiguration
  • 配置类上及/或bean定义上标注条件(@ConditionalOnXxx系列注解)
  • 将配置类类名写入到文件/META-INF/spring.factories(.properties格式)中的键org.springframework.boot.autoconfigure.EnableAutoConfiguration=的值中(多值以逗号分隔)。
  • 配置条件所依赖的三方类在pom.xml中定义为非传递依赖,即<dependency><optional>true</optional></...>
  • 独立成包,不与业务代码在同一项目。

@Conditional(ConditionImpl.class) 条件行为(满足条件才创建bean,spring boot常用),条件接口Condition。


一种Java Bean到另一种Bean的自动映射工具:Dozer

Spring Boot Data

注解@Enable*Repository <--> 类RepositoryBeanDefinitionRegistrarSupport

Spring Boot Keycloak


import lombok.val;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@KeycloakConfiguration
public class WebSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

	@Autowired
	 public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		val keycloakAuthenticationProvider = keycloakAuthenticationProvider();
		keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
		auth.authenticationProvider(keycloakAuthenticationProvider);
	}

	@Bean
	public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
		return new KeycloakSpringBootConfigResolver();
	}

	@Override
	protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
		return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		super.configure(http);

		http
				.cors()

				.and()
				.authorizeRequests()
				.antMatchers("/static/**").permitAll()
				.anyRequest().authenticated()

				.and()
				.csrf().disable();
	}
	
}

Please note the code above:

  • configureGlobal: tasks the SimpleAuthorityMapper to make sure roles are not prefixed with ROLE_
  • keycloakConfigResolver: this defines that we want to use the Spring Boot properties file support instead of the default keycloak.json
keycloak:
  auth-server-url: http://127.0.0.1:9150/auth
  realm: master
  resource: prsystem
  public-client: true
  principal-attribute: preferred_username
  #use-resource-role-mappings: true     #If set to true, the adapter will look inside the token for application level role mappings for the user. If false, it will look at the realm level for user role mappings. The default value is false.
  bearer-only: true

bearer-only: true 使得未认证访问资源返回401,否则将返回302重定向到/sso/login。

Keycloak security constraints:

keycloak.security-constraints[0].authRoles[0]=user
keycloak.security-constraints[0].securityCollections[0].patterns[0]=/customers/*

use-resource-role-mappings: true true:使用client中的role;false:使用realm中的。

Disable keycloak for cases like testing:

keycloak.enabled: false

Spring Boot Actuator

dependencies:

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

REST api for scheduled tasks:

management.endpoints.web.exposure.include: scheduledtasks

Spring Web

自定义类型作为request参数类型或返回类型,如何注册解析器或转换器?

返回的字符串消息(如错误信息)适应多语言环境??

以下的“响应方法”controller类中响应web请求的java方法。@注解 后加的(for class/method/parameter等)指的是该注解用在类、响应方法、响应方法参数等位置。

Web服务器配置:

#服务端口
server.port: 8080

#context-path,是服务器为其中所有servlet请求地址添加的前缀
server.servlet.context-path: "/api"    #默认"/"。spring-boot 1.x的配置键为servlet.context-path

#内置Web容器(默认tomcat)处理请求的并发数
# For Tomcat
server.tomcat.max-threads: 1024
# For Undertow
server.undertow.worker-threads: 1024
# For Jetty
server.jetty.acceptors: 1024

@RequestMapping("/xxx") for controller class(类上标注的), 含义是该controller接受的响应请求路径的前缀,加上@RequestMapping("yyy") for method(方法上标注的)的路径形成最终能响应的请求路径。spring不支持方法上的注解忽略类上注解中的路径,也就说当一个控制器中的很多处理方法都具有某种路径前缀,因此把该路径前缀标注到类上时,而某个方法不能具有该前缀时,没有一种策略使得该方法可以放弃继承类上的路径前缀。

@RequestMapping(method=...)可设置可响应的Http方法(GET,POST等),同时也有相应的@XxxMapping注解快捷表示方法,其中“Xxx”表示方法名,如有@GetMapping, @PostMapping等。

@RequestMapping中未设置method时,表示可接受所有Http方法。

@GetMapping @PostMapping等相当于method为对应的GET,POST等的@RequestMapping。

返回文件流:为请求处理方法标注@RequestMapping(produces=MediaType.APPLICATION_OCTET_STREAM_VALUE),并返回适当类型的数据,如字节数组byte[],或org.springframework.core.io.Resource。若想返回InputStream,可用org.springframework.core.io.InputStreamResource#InputStreamResource(java.io.InputStream)将其包装,否则导致HTTP响应流的content-length不正确,为0(版本2.1之前),或content-type不正确,为application/json(spring boot版本2.1.x)。

@RequestMapping(value=Array("/file"), produces=Array(MediaType.APPLICATION_OCTET_STREAM_VALUE))
def getFile() = {
    val headers = new HttpHeaders()
    headers.set(HttpHeaders.CONTENT_DISPOSITION, s"attachment; filename=filename.json")    //设置浏览器显示的文件名
    
    val data: Array[Byte] = "hello".getBytes        // byte[]或org.springframework.core.io.Resource
    
    new HttpEntity(data, headers)
    
    new HttpEntity(new InputStreamResource(new ByteArrayInputStream(data): InputStream, headers)
}

如果响应的java方法参数中有原子类型(int,boolean等),那么web请求中必须提供该参数,否则报错,如果要实现参数可选,使用对应的包装类型(Integer, Boolean),对象类型参数在web请求中未提供时值为null。

请求参数是多值型时,使用对应类型的数组类型(T[])或集合类型(List,Set,Collection)。

Restful请求中的路径参数定义使用花括号包裹,如@RequestMapping("/user/info/{userId}"),参数值捕获使用 void f(@PathVariable("userId") String userId)。

@CookieValue(value="JSESSIONID", defaultValue="")(for parameter),获取Cookie中的值 。

@RequestParam(name/value="paramName", required=true|false, defaultValue="")(for parameter)标记的参数为表单(application/x-www-form-urlencoded)提交方式下web request body中的字段或URL中的参数。
@RequestParam可标记Map,绑定所有参数和值。

@SessionAttribute (for parameter) 获取HttpSession中的属性。

@SessionAttributes (for class)

@ModelAttribute (for method)

@RequestBody (for parameter,只准用一次)标记类型为POJO的响应方法参数,要求web请求的content-type为application/json,需要一个从json格式字符串转换到POJO的解析器,一般用com.aliababa:fastjson或jackson。
@RequestBody可以标记Map,绑定所有键值。

@RequestBody可与@RequestParam同时使用,content-type要求为application/json,@RequestBody标记的POJO由web rquest body中的json格式串解析而来,@RequestParam标记的参数由URL参数解析而来。

@PathVariable(for parameter)获取URL中访问路径部分中的变量。如@RequestMapping("/book/{type}")中的"type"变量。后接冒号:加上取值限制,如限制值在A,B,C中选一个@PathVariable("/{type:A|B|C}")

@PathParam(for parameter)获取URL中问号后面的指定参数。如"/book?id=xxx"中的"id"的值。

@RequestParam (for parameter)获取URL中的查询参数键或表单指定参数名的值。

HandlerMethodArgumentResolver:自定义参数解析器。

示例:(web query parameter参数解析,带方括号的数组)

@Component
class WebConfig extends AnyRef with WebMvcConfigurer {
  override def addArgumentResolvers(resolvers: java.util.List[HandlerMethodArgumentResolver]): Unit = resolvers.add(new BeCsvReqParamMethodArgumentHandler)
}

class BeCsvReqParamMethodArgumentHandler extends AbstractNamedValueMethodArgumentResolver with UriComponentsContributor {

  def createNamedValueInfo(parameter: MethodParameter): AbstractNamedValueMethodArgumentResolver.NamedValueInfo = {
    val ann = parameter.getParameterAnnotation(classOf[BeCsvReqParam])
    if (ann == null) new AbstractNamedValueMethodArgumentResolver.NamedValueInfo("", false, ValueConstants.DEFAULT_NONE)
    else new AbstractNamedValueMethodArgumentResolver.NamedValueInfo(ann.name(), ann.required(), ann.defaultValue())
  }

  def resolveName(name: String, parameter: MethodParameter, request: NativeWebRequest): AnyRef = {
    val v = request.getParameterValues(name)
    v match {
      case null => null
        
      case Array(v0) =>
        if (v0.startsWith("[") && v0.endsWith("]")) v0.substring(1, v0.length - 1)
        else v0
        
      case _ => v
    }
  }

  def supportsParameter(parameter: MethodParameter): Boolean = {
    parameter.hasParameterAnnotation(classOf[BeCsvReqParam])
  }

  // see RequestParamMethodArgumentResolver
}

//Java

import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.ValueConstants;
public @interface DateTimeYMDH {
	public @interface BeCsvReqParam {
		@AliasFor("name") String value() default "";
		@AliasFor("value") String name() default "";
		boolean required() default true;
		String defaultValue() default ValueConstants.DEFAULT_NONE;
	}
}

如果ajax请求时传入的是json对象,响应方法中的参数是用@RequestParam标记的,而这样的方式能成功请求/响应,则需确认浏览器实际传输请求时用的content-type是application/json还是application/x-www-form-urlencoded,另外查看ajax使用的JS框架有没有将json对象自动转为URL参数或转为form表单形式提交(如果默认的ajax请求的content-type是application/x-www-form-urlencoded,JS框架可能会这么做,jQuery就是个例子)。

条件性地定义Controller:利用系列注解@ConditionalOnXxx,仅对类有效,对类方法不起作用。

RestTemplate:该类可用作web请求客户端,线程安全。其.get*().post*()等方法对应使用HttpMethod GET, POST等方式,其中有参数(String url,..., Map<String,> uriVariables),键值对参数uriVariables用于扩展url中的“变量”(以花括号裹挟命名),而不仅仅请求参数(url中问号?后的键值参数),该参数不是将uriVariables中所有键值对拼接为url的请求参数。如url="http://127.0.0.1/?foo={foo}"时会将Map uriVariable中键为foo的值扩展到url中,假设foo对应值为myval,则为http://127.0.0.1/?foo=myval,而如果url="http://127.0.0.1/",则不会得到http://127.0.0.1/?foo=myval。url参数中的待扩展“变量”可以定义到任意位置(路径的一部分、端口等),而不限于请求参数。

定义RestTemplate实例可通过RestTemplateBuilder实现,后者的bean在引入依赖spring-boot-starter-web后自动定义,RestTemplateBuilder的每个配置方法(如defaultHeader(), basicAuthentication()等)将返回一个新的RestTemplateBuilder实例,因此是线程安全的(也就说在多个地方自动注入RestTemplateBuilder时也能正确构建各自的RestTemplate)。

控制器增强 controller advice
利用AOP对增强控制器,如对特定类型异常进行统一处理。

控制器异常处理器(exception handler):定义控制器增强。
使用@ExceptionHandler(Array[Exception])标注方法,使方法成为增强方法,在方法参数列表中定义相应的Exception类型参数以捕获被抛出的异常,定义WebRequest捕获web请求。

WebRequest.getContextPath返回空串? <== context-path是应用部署时为全部servlet请求地址设置的额外前缀。如果需要请求servlet路径,需捕获HttpServletRequest,调用其.getServletPath()方法获取。

MissingServletRequestParameterException:该异常由spring在发现接口要求的参数在被请求时实际没有提供的情况时抛出。

@RestControllerAdvice是全局性的。

import java.io.IOException

import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.web.bind.MissingServletRequestParameterException
import org.springframework.web.bind.annotation.{ExceptionHandler, ResponseStatus, RestControllerAdvice}
import org.springframework.web.context.request.WebRequest

@RestControllerAdvice
class ControllerExceptionHandler {
  private final val log = LoggerFactory.getLogger(getClass)

  @ExceptionHandler(Array(classOf[IOException]))
  //@ResponseStatus(HttpStatus.?) //定义返回状态码
  def ioExceptionHandler(e: IOException, webRequest: WebRequest) = {   //在示例项目中,DataPackage类型是Controller方法的返回类型,实质是JSON(java.util.Map)
    //捕获抛出异常的控制器类,使用该类对应的logger来打日志?  <== 似乎无法捕获该控制器类。
    log.error("io err, request path: {}, params: {}", webRequest.getContextPath, wrapParamValueArray(webRequest.getParameterMap), e)
    
    return "fail"
  }

  // 如果是单值则将类型变为字符串,如果多值,则转为java.util.List。这么转换是因为数组的.toString不会输出各元素值,而List会。
  def wrapParamValueArray(params: java.util.Map[String, Array[String]]): java.util.Map[String, AnyRef] = {
    val wrapped = new java.util.HashMap[String, AnyRef]()
    params.keySet().forEach(key => {
      val v = params.get(key)
      if (v != null && v.length == 1) {
        wrapped.put(key, v(0))
      } else if (v != null && v.length > 1) { // to list
        wrapped.put(key, java.util.Arrays.asList(v: _*))
      } else { // null or empty
        wrapped.put(key, null)
      }
    })
    wrapped
  }

  @ExceptionHandler(Array(classOf[MissingServletRequestParameterException]))
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  def missingRequestParamExceptionHandler(e: MissingServletRequestParameterException): DataPackage = {
    DataPackage.fail().setMsg(s"missing parameter '${e.getParameterName}'")
  }
}

Java版:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

@RestControllerAdvice
public class ControllerExceptionHandler {
	private final Logger log = LoggerFactory.getLogger(getClass());

	@ExceptionHandler(Exception.class)
	public DataPackage exceptionHandler(Exception e, HttpServletRequest req) {
		if (log.isErrorEnabled() && req != null) {
			log.error("err. request path: {}, params: {}", req.getServletPath(), wrapParamValueArray(req.getParameterMap()), e);
		}
		return DataPackage.fail();
	}

	public java.util.Map<String, Object> wrapParamValueArray(java.util.Map<String, String[]> params) {
		java.util.Map<String, Object> wrapped = new java.util.HashMap<>();
		params.keySet().forEach(key -> {
			String[] v = params.get(key);
			if (v != null && v.length == 1) {
				wrapped.put(key, v[0]);
			} else if (v != null && v.length > 1) { // to list
				wrapped.put(key, java.util.Arrays.asList(v));
			} else { // null or empty
				wrapped.put(key, null);
			}
		});
		return wrapped;
	}
}

可为一个controller class指定异常处理器,通过对controller class 中的一个方法标注@ExceptionHandler实现。

@RestController
public class MyController {
    @RequestMapping(...)
    public Object handleRequest() {
        ...
    }
    
    @ExceptionHandler
    @HttpStatus(HttpStatus.BAD_REQUEST)
    public Object handleException(Exceptin e) {
        return "bad argument in request".
    }
}

Spring Caching

@EnableCaching开启基于注解的缓存。

一个缓存具有一个名字,缓存系统里存在多个缓存容器(接口Cache),以名字辨别。

@Cacheable 标注于方法或类型(相当于标注其下所有方法),参数:

  • cacheNames 缓存名字
  • key SpEL表达式以决定缓存的key,默认为被标注方法的所有参数。
  • condition 一个返回boolean的SpEL表达式,以判断被标注方法在被调用是否需要对数据进行缓存。
  • unless 一个返回boolean的SPEL表达式,以判断被标注方法在被调用是否需要对数据进行缓存。
  • keyGenerator 一个bean的名字,以作为key生成器,即决定如何将被标注方法的参数生成为cache的key。(与key互斥)
  • cacheManager bean名字,以作为CacheManager(与cacheResolver互斥)
  • cacheResolver bean名字,以作为CacheResolver(与cacheManager互斥)

SpEL上下文:

  • root.methodName 方法名
  • root.method Method对象
  • root.target 持有被调用方法的类实例
  • root.targetClass 持有被调用方法的类
  • root.args 被调用方法的参数列表
  • caches 方法使用的缓存列表(或需要影响的缓存列表)
  • <参数名字> 可通过#<方法参数名>访问方法参数
  • result 方法被调用后的返回结果(仅对作用于方法调用后的情况有效,如condition, cacheEvict等)

Spring Logging

日志级别动态设置支持:

application.yml

management.endpoints.web.exposure.include: loggers
management.endpoint.loggers.enabled: true

pom.xml

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

内置web容器的web请求DEBUG日志查看:

  • org.apache.coyote.http11.Http11InputBuffer:请求路径及接收到的数据(包括请求的数据体)。
  • org.springframework.web.servlet.DispatcherServlet:请求路径及请求参数。

Spring Web Security: 默认开启了CSRF,如果在请求中没有加入csr相应参数,则可能出现403错误。

spring aspect AOP

打印所有HTTP接口的耗时:

public class WebLogTimeCost {
    @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public Object doAround(ProceedingJoinPoint jpt) {
        long s = System.currentTimeMillis();
        Object returnVal = jpt.proceed();
        long tmc = System.currentTimeMillis();
        String typeName = sig.getDeclaringTypeName();
		String methodName = sig.getName();
        
        System.out.println(String.format("time cost: %s, method: %s, type: %s", tmc, methodName, typeName));
        
        return returnVal;
    }
}

spring boot test

<!--pom.xml-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>${spring.boot.version}</version>
    <scope>test</scope>
</dependency>
// with junit
@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppMain.class)
@WebAppConfiguration  // for spring boot web, or @SpringBootTest(classes=, webEnvironment=)
public class AppTest {
}

public class WebTest extends AppTest {
	@Autowired
	WebApplicationContext webApplicationContext;

	MockMvc mockMvc;

	@Before
	public void setUpMockMvc() {
		mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}

测试组件(没有main入口类)如业务JpaRepo类

@RunWith(SpringRunner.class)
@SpringBootTest(classes=MyJpaConfig.class)
@EnableAutoConfiguration
//@EnableApolloConfig("my-namespace") //如果需要使用ctrip-apollo
public class MyTest{}

定义测试类间测试顺序:

定义测试类下测试方法的测试顺序:
通过junit定义。

可能遇到的问题

java.lang.NoClassDefFoundError: Could not initialize class com.fasterxml.jackson.databind.ObjectMapper
可能因引入多个版本的jackson库导致。

java.lang.NoClassDefFoundError: org/hibernate/validator/internal/engine/DefaultClockProvider
at org.hibernate.validator.internal.engine.ConfigurationImpl.
可能由javax.validation:validation-api与org.hibernate:hibernate-validator的版本问题导致。

posted @ 2019-05-14 18:49  二球悬铃木  阅读(432)  评论(0编辑  收藏  举报