init project

This commit is contained in:
linpl 2023-02-22 10:26:20 +08:00
commit 03b6ae2491
39 changed files with 1584 additions and 0 deletions

40
.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
.idea

79
example/pom.xml Normal file
View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.linpl</groupId>
<artifactId>example</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mapstruct.version>1.5.1.Final</mapstruct.version>
<mapstruct-plus.version>1.0.0</mapstruct-plus.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.github.linpl</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
<version>${mapstruct-plus.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>io.github.linpl</groupId>
<artifactId>mapstruct-plus-processor</artifactId>
<version>${mapstruct-plus.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>io.github.linpl</groupId>
<artifactId>mapstruct-plus-processor</artifactId>
<version>${mapstruct-plus.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,17 @@
package io.github.linpl.mapstruct;
import io.github.linpl.annotations.AutoMapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.convert.ConversionService;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@ -0,0 +1,7 @@
package io.github.linpl.mapstruct;
import io.github.linpl.annotations.MapperConfig;
@MapperConfig(mapperPackage = "io.github.linpl.mapstruct.convert")
public class MapstructConfiguration {
}

View File

@ -0,0 +1,29 @@
package io.github.linpl.mapstruct;
import io.github.linpl.Converter;
import io.github.linpl.mapstruct.dto.Car;
import io.github.linpl.mapstruct.dto.CarDto;
import io.github.linpl.mapstruct.dto.CarType;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Component;
@Component
public class Startup {
private Converter converter;
@PostConstruct
public void start() {
Car car = new Car();
car.setType(CarType.OTHER);
CarDto catDto = converter.convert(car, CarDto.class);
System.out.println(catDto);
}
@Autowired
public void setConverter(final Converter converter) {
this.converter = converter;
}
}

View File

@ -0,0 +1,47 @@
package io.github.linpl.mapstruct.dto;
import io.github.linpl.annotations.AutoMapper;
import io.github.linpl.annotations.AutoMapping;
@AutoMapper(target = CarDto.class)
public class Car {
private String make;
private SeatConfiguration seatConfiguration;
private CarType type;
@AutoMapping(target = "wheels", ignore = true)
private Wheels wheels;
public String getMake() {
return make;
}
public void setMake(final String make) {
this.make = make;
}
public SeatConfiguration getSeatConfiguration() {
return seatConfiguration;
}
public void setSeatConfiguration(final SeatConfiguration seatConfiguration) {
this.seatConfiguration = seatConfiguration;
}
public CarType getType() {
return type;
}
public void setType(final CarType type) {
this.type = type;
}
public Wheels getWheels() {
return wheels;
}
public void setWheels(Wheels wheels) {
this.wheels = wheels;
}
}

View File

@ -0,0 +1,52 @@
package io.github.linpl.mapstruct.dto;
import java.util.List;
public class CarDto {
private String make;
private SeatConfigurationDto seatConfiguration;
private String type;
private List<WheelDto> wheels;
public String getMake() {
return make;
}
public void setMake(final String make) {
this.make = make;
}
public SeatConfigurationDto getSeatConfiguration() {
return seatConfiguration;
}
public void setSeatConfiguration(final SeatConfigurationDto seatConfiguration) {
this.seatConfiguration = seatConfiguration;
}
public String getType() {
return type;
}
public void setType(final String type) {
this.type = type;
}
public List<WheelDto> getWheels() {
return wheels;
}
public void setWheels(List<WheelDto> wheels) {
this.wheels = wheels;
}
@Override
public String toString() {
return "CarDto{" +
"make='" + make + '\'' +
", seats=" + seatConfiguration +
", type='" + type + '\'' +
", wheels=" + wheels +
'}';
}
}

View File

@ -0,0 +1,5 @@
package io.github.linpl.mapstruct.dto;
public enum CarType {
SPORTS, OTHER
}

View File

@ -0,0 +1,25 @@
package io.github.linpl.mapstruct.dto;
import io.github.linpl.annotations.AutoMapper;
@AutoMapper(target = SeatConfigurationDto.class)
public class SeatConfiguration {
private int numberOfSeats;
private SeatMaterial seatMaterial;
public SeatMaterial getSeatMaterial() {
return seatMaterial;
}
public void setSeatMaterial(final SeatMaterial seatMaterial) {
this.seatMaterial = seatMaterial;
}
public int getNumberOfSeats() {
return numberOfSeats;
}
public void setNumberOfSeats(final int numberOfSeats) {
this.numberOfSeats = numberOfSeats;
}
}

View File

@ -0,0 +1,22 @@
package io.github.linpl.mapstruct.dto;
public class SeatConfigurationDto {
private int seatCount;
private String material;
public int getSeatCount() {
return seatCount;
}
public void setSeatCount(final int seatCount) {
this.seatCount = seatCount;
}
public String getMaterial() {
return material;
}
public void setMaterial(final String material) {
this.material = material;
}
}

View File

@ -0,0 +1,5 @@
package io.github.linpl.mapstruct.dto;
public enum SeatMaterial {
LEATHER, FABRIC
}

View File

@ -0,0 +1,39 @@
package io.github.linpl.mapstruct.dto;
import io.github.linpl.annotations.AutoMapper;
import java.util.Objects;
@AutoMapper(target = WheelDto.class)
public class Wheel {
private WheelPosition position;
private int diameter;
public WheelPosition getPosition() {
return position;
}
public void setPosition(WheelPosition position) {
this.position = position;
}
public int getDiameter() {
return diameter;
}
public void setDiameter(int diameter) {
this.diameter = diameter;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Wheel wheel = (Wheel) o;
return diameter == wheel.diameter && position == wheel.position;
}
@Override
public int hashCode() {
return Objects.hash(position, diameter);
}
}

View File

@ -0,0 +1,37 @@
package io.github.linpl.mapstruct.dto;
import java.util.Objects;
public class WheelDto {
private String position;
private int diameter;
public void setDiameter(int diameter) {
this.diameter = diameter;
}
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
public int getDiameter() {
return diameter;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WheelDto wheelDto = (WheelDto) o;
return diameter == wheelDto.diameter && Objects.equals(position, wheelDto.position);
}
@Override
public int hashCode() {
return Objects.hash(position, diameter);
}
}

View File

@ -0,0 +1,8 @@
package io.github.linpl.mapstruct.dto;
public enum WheelPosition {
LEFT_FRONT,
RIGHT_FRONT,
LEFT_REAR,
RIGHT_REAR
}

View File

@ -0,0 +1,38 @@
package io.github.linpl.mapstruct.dto;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.function.Consumer;
public class Wheels implements Iterable<Wheel> {
private List<Wheel> wheelsList = new ArrayList<>();
public List<Wheel> getWheelsList() {
return wheelsList;
}
public void setWheelsList(List<Wheel> wheelsList) {
this.wheelsList = wheelsList;
}
public void add(final Wheel wheel) {
wheelsList.add(wheel);
}
@Override
public Iterator<Wheel> iterator() {
return getWheelsList().iterator();
}
@Override
public void forEach(Consumer<? super Wheel> action) {
getWheelsList().forEach(action);
}
@Override
public Spliterator<Wheel> spliterator() {
return getWheelsList().spliterator();
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mapstruct-plus</artifactId>
<groupId>io.github.linpl</groupId>
<version>${mapstruct-plus.version}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mapstruct-plus-core</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,11 @@
package io.github.linpl;
import org.mapstruct.MappingTarget;
public interface BaseMapper<S, T> {
T convert(S source);
T convert(S source, @MappingTarget T target);
}

View File

@ -0,0 +1,12 @@
package io.github.linpl;
public class ConvertException extends RuntimeException {
public ConvertException(final String message) {
super(message);
}
public ConvertException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,51 @@
package io.github.linpl;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class Converter {
private final ConverterFactory converterFactory;
public Converter(final ConverterFactory converterFactory) {
this.converterFactory = converterFactory;
}
@SuppressWarnings("unchecked")
public <S, T> T convert(S source, Class<T> targetType) {
if (source == null) {
return null;
}
BaseMapper<S, T> mapper = (BaseMapper<S, T>) converterFactory.getMapper(source.getClass(), targetType);
if (mapper != null) {
return mapper.convert(source);
}
throw new ConvertException(
"cannot find converter from " + source.getClass().getSimpleName() + " to " + targetType.getSimpleName());
}
@SuppressWarnings("unchecked")
public <S, T> T convert(S source, T target) {
if (source == null) {
return null;
}
if (target == null) {
return null;
}
BaseMapper<S, T> mapper = (BaseMapper<S, T>) converterFactory.getMapper(source.getClass(), target.getClass());
if (mapper != null) {
return mapper.convert(source);
}
throw new ConvertException("cannot find converter from " + source.getClass().getSimpleName() + " to " +
target.getClass().getSimpleName());
}
public <S, T> List<T> convert(List<S> source, Class<T> targetType) {
if (source == null || source.size() == 0) {
return Collections.emptyList();
}
return source.stream().map(item -> convert(item, targetType)).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,7 @@
package io.github.linpl;
public interface ConverterFactory {
<S, T> BaseMapper<S, T> getMapper(Class<S> sourceType, Class<T> targetType);
}

View File

@ -0,0 +1,18 @@
package io.github.linpl.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface AutoMapper {
Class<?> target();
Class<?>[] uses() default {};
Class<?>[] imports() default {};
}

View File

@ -0,0 +1,24 @@
package io.github.linpl.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface AutoMapping {
String target();
String dateFormat() default "";
String numberFormat() default "";
String constant() default "";
String expression() default "";
boolean ignore() default false;
}

View File

@ -0,0 +1,24 @@
package io.github.linpl.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 在一个类或者接口上添加该注释作为自动生成 Mapper 的配置类在一个模块中只能有一个有该注释的类
* Marks a class or interface as configuration source. There can be only one annotated type in
* each compiled module.
*
* @author linpl
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface MapperConfig {
/**
* 所生成的 Mapper 接口的包
*/
String mapperPackage() default "";
}

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mapstruct-plus</artifactId>
<groupId>io.github.linpl</groupId>
<version>${mapstruct-plus.version}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mapstruct-plus-processor</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>io.github.linpl</groupId>
<artifactId>mapstruct-plus-core</artifactId>
</dependency>
<dependency>
<groupId>com.baidu.lbsyun</groupId>
<artifactId>javapoet</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<proc>none</proc>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,89 @@
package io.github.linpl.processor;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeSpec;
import java.io.IOException;
import java.io.Writer;
import java.util.Collection;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Modifier;
import static javax.tools.Diagnostic.Kind.ERROR;
public class AdapterMapperGenerator {
public void write(ProcessingEnvironment processingEnv, Collection<AdapterMethodMetadata> adapterMethods) {
// write Adapter
try (final Writer writer = processingEnv.getFiler()
.createSourceFile(
AutoMapperProperties.getAdapterPackage() + "." + AutoMapperProperties.getAdapterClassName())
.openWriter()) {
JavaFile.builder(AutoMapperProperties.getAdapterPackage(), createTypeSpec(adapterMethods))
.build()
.writeTo(writer);
} catch (IOException e) {
processingEnv.getMessager()
.printMessage(ERROR,
"Error while opening " + AutoMapperProperties.getAdapterClassName() + " output file: " +
e.getMessage());
}
}
private TypeSpec createTypeSpec(Collection<AdapterMethodMetadata> adapterMethods) {
TypeSpec.Builder adapterBuilder = TypeSpec.classBuilder(
ClassName.get(AutoMapperProperties.getAdapterPackage(), AutoMapperProperties.getAdapterClassName()))
.addModifiers(Modifier.PUBLIC)
.addAnnotation(ClassName.get("org.springframework.stereotype", "Component"));
adapterMethods.forEach(adapterMethod -> adapterBuilder.addField(buildMapperField(adapterMethod.getMapper()))
.addMethod(buildMapperSetterMethod(adapterMethod.getMapper()))
.addMethod(buildProxyMethod(adapterMethod)));
return adapterBuilder.build();
}
private FieldSpec buildMapperField(ClassName mapper) {
return FieldSpec.builder(mapper,
firstWordToLower(mapper.simpleName()), Modifier.PRIVATE).build();
}
private String firstWordToLower(String str) {
return str.substring(0, 1).toLowerCase() + str.substring(1);
}
private MethodSpec buildProxyMethod(AdapterMethodMetadata adapterMethodMetadata) {
ParameterSpec parameterSpec = ParameterSpec.builder(adapterMethodMetadata.getSource(),
firstWordToLower(adapterMethodMetadata.getSource().simpleName())).build();
return MethodSpec.methodBuilder(firstWordToLower(adapterMethodMetadata.getSource().simpleName()) + "To" +
adapterMethodMetadata.getTarget().simpleName())
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterSpec)
.returns(adapterMethodMetadata.getTarget())
.addStatement("return $N.convert($N)", firstWordToLower(adapterMethodMetadata.getMapper().simpleName()),
firstWordToLower(adapterMethodMetadata.getSource().simpleName()))
.build();
}
private MethodSpec buildMapperSetterMethod(ClassName mapper) {
ParameterSpec parameterSpec = buildMapperSetterParameter(mapper);
return MethodSpec.methodBuilder("set" + mapper.simpleName())
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterSpec)
.addStatement("this.$N = $N", buildMapperField(mapper), parameterSpec)
.build();
}
private ParameterSpec buildMapperSetterParameter(ClassName mapper) {
return ParameterSpec.builder(mapper,
firstWordToLower(mapper.simpleName()))
.addAnnotation(
AnnotationSpec.builder(ClassName.get("org.springframework.context.annotation", "Lazy")).build())
.build();
}
}

View File

@ -0,0 +1,39 @@
package io.github.linpl.processor;
import com.squareup.javapoet.ClassName;
public class AdapterMethodMetadata {
private AdapterMethodMetadata(final ClassName source, final ClassName target, ClassName mapper) {
this.source = source;
this.target = target;
this.mapper = mapper;
}
private final ClassName source;
private final ClassName target;
private final ClassName mapper;
public static AdapterMethodMetadata newInstance(ClassName source, ClassName target, ClassName mapper) {
return new AdapterMethodMetadata(source, target, mapper);
}
public String getMethodName() {
return source.simpleName().substring(0, 1).toLowerCase() + source.simpleName().substring(1) + "To" +
target.simpleName();
}
public ClassName getSource() {
return source;
}
public ClassName getTarget() {
return target;
}
public ClassName getMapper() {
return mapper;
}
}

View File

@ -0,0 +1,112 @@
package io.github.linpl.processor;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.lang.model.element.Modifier;
import org.apache.commons.lang3.StringUtils;
import static io.github.linpl.processor.Constants.*;
public class AutoMapperGenerator {
public void write(AutoMapperMetadata metadata, Writer writer) {
try {
JavaFile.builder(AutoMapperProperties.getMapperPackage(), createTypeSpec(metadata)).build().writeTo(writer);
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (Exception e) {
throw e;
}
}
private TypeSpec createTypeSpec(AutoMapperMetadata metadata) {
ParameterizedTypeName converterName =
ParameterizedTypeName.get(ClassName.get(BASE_MAPPER_PACKAGE, BASE_MAPPER_CLASS_NAME),
metadata.getSourceClassName(), metadata.getTargetClassName());
TypeSpec.Builder builder = TypeSpec.interfaceBuilder(metadata.mapperName())
.addSuperinterface(converterName)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(buildGeneratedMapperAnnotationSpec(metadata));
if (metadata.getFieldMappingList() != null && !metadata.getFieldMappingList().isEmpty()) {
final ParameterSpec source = ParameterSpec.builder(metadata.getSourceClassName(), "source").build();
final ParameterSpec target = ParameterSpec.builder(metadata.getTargetClassName(), "target")
.addAnnotation(AnnotationSpec.builder(ClassName.get("org.mapstruct", "MappingTarget")).build())
.build();
builder.addMethod(addConvertMethodSpec(Collections.singletonList(source), metadata.getFieldMappingList(),
metadata.getTargetClassName()));
builder.addMethod(addConvertMethodSpec(Arrays.asList(source, target), metadata.getFieldMappingList(),
metadata.getTargetClassName()));
}
return builder.build();
}
private MethodSpec addConvertMethodSpec(List<ParameterSpec> parameterSpecs,
List<AutoMappingMetadata> autoMappingMetadataList,
ClassName target) {
return MethodSpec.methodBuilder("convert")
.addParameters(parameterSpecs)
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addAnnotations(buildMappingAnnotations(autoMappingMetadataList))
.returns(target)
.build();
}
private List<AnnotationSpec> buildMappingAnnotations(final List<AutoMappingMetadata> autoMappingMetadataList) {
return autoMappingMetadataList.stream().map(autoMappingMetadata -> {
final AnnotationSpec.Builder builder = AnnotationSpec.builder(ClassName.get("org.mapstruct", "Mapping"))
.addMember("target", CodeBlock.builder().add("$S", autoMappingMetadata.getTarget()).build())
.addMember("dateFormat",
CodeBlock.builder().add("$S", autoMappingMetadata.getDateFormat()).build())
.addMember("numberFormat",
CodeBlock.builder().add("$S", autoMappingMetadata.getNumberFormat()).build())
.addMember("ignore", CodeBlock.builder().add(String.valueOf(autoMappingMetadata.isIgnore())).build());
if (StringUtils.isNoneEmpty(autoMappingMetadata.getExpression())) {
builder.addMember("expression",
CodeBlock.builder().add("$S", autoMappingMetadata.getExpression()).build());
} else {
builder.addMember("source", CodeBlock.builder().add("$S", autoMappingMetadata.getSource()).build());
}
return builder.build();
}).collect(Collectors.toList());
}
private AnnotationSpec buildGeneratedMapperAnnotationSpec(AutoMapperMetadata metadata) {
List<ClassName> usesClassNameList =
Optional.ofNullable(metadata.getUsesClassNameList()).orElse(new ArrayList<>());
// config
CodeBlock configCodeBlock = CodeBlock.builder()
.add("$T.class",
ClassName.get(AutoMapperProperties.getMapperPackage(), AutoMapperProperties.getConfigClassName()))
.build();
// uses
CodeBlock.Builder usesCodeBuilder = CodeBlock.builder().add("{");
usesClassNameList.forEach(item -> usesCodeBuilder.add("$T.class", item));
CodeBlock usesCodeBlock = usesCodeBuilder.add("}").build();
AnnotationSpec.Builder builder =
AnnotationSpec.builder(ClassName.get(MAPSTRUCT_MAPPER_PACKAGE, MAPSTRUCT_MAPPER_CLASS_NAME))
.addMember("config", configCodeBlock)
.addMember("uses", usesCodeBlock);
return builder.build();
}
}

View File

@ -0,0 +1,55 @@
package io.github.linpl.processor;
import com.squareup.javapoet.ClassName;
import java.util.List;
public class AutoMapperMetadata {
private ClassName sourceClassName;
private ClassName targetClassName;
private List<ClassName> usesClassNameList;
private List<AutoMappingMetadata> fieldMappingList;
public String mapperName() {
return sourceClassName.simpleName() + "To" + targetClassName.simpleName() + "Mapper";
}
public ClassName getSourceClassName() {
return sourceClassName;
}
public AutoMapperMetadata setSourceClassName(final ClassName sourceClassName) {
this.sourceClassName = sourceClassName;
return this;
}
public ClassName getTargetClassName() {
return targetClassName;
}
public AutoMapperMetadata setTargetClassName(final ClassName targetClassName) {
this.targetClassName = targetClassName;
return this;
}
public List<ClassName> getUsesClassNameList() {
return usesClassNameList;
}
public AutoMapperMetadata setUsesClassNameList(final List<ClassName> usesClassNameList) {
this.usesClassNameList = usesClassNameList;
return this;
}
public List<AutoMappingMetadata> getFieldMappingList() {
return fieldMappingList;
}
public AutoMapperMetadata setFieldMappingList(final List<AutoMappingMetadata> fieldMappingList) {
this.fieldMappingList = fieldMappingList;
return this;
}
}

View File

@ -0,0 +1,238 @@
package io.github.linpl.processor;
import com.squareup.javapoet.ClassName;
import io.github.linpl.annotations.AutoMapper;
import io.github.linpl.annotations.AutoMapping;
import io.github.linpl.annotations.MapperConfig;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.MirroredTypesException;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import org.apache.commons.lang3.StringUtils;
import static io.github.linpl.processor.Constants.AUTO_MAPPER_ANNOTATION;
import static io.github.linpl.processor.Constants.MAPPER_CONFIG_ANNOTATION;
import static javax.tools.Diagnostic.Kind.ERROR;
@SupportedAnnotationTypes({AUTO_MAPPER_ANNOTATION, MAPPER_CONFIG_ANNOTATION})
public class AutoMapperProcessor extends AbstractProcessor {
private final AutoMapperGenerator mapperGenerator;
private final AdapterMapperGenerator adapterMapperGenerator;
private final MapperConfigGenerator mapperConfigGenerator;
private final Map<String, AdapterMethodMetadata> methodMap = new HashMap<>();
private final Set<String> mapperSet = new HashSet<>();
public AutoMapperProcessor() {
this.mapperGenerator = new AutoMapperGenerator();
this.adapterMapperGenerator = new AdapterMapperGenerator();
this.mapperConfigGenerator = new MapperConfigGenerator();
}
private boolean isAutoMapperAnnotation(TypeElement annotation) {
return AUTO_MAPPER_ANNOTATION.contentEquals(annotation.getQualifiedName());
}
private boolean isMapperConfigAnnotation(TypeElement annotation) {
return MAPPER_CONFIG_ANNOTATION.contentEquals(annotation.getQualifiedName());
}
@Override
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
boolean hasAutoMapper = annotations.stream().anyMatch(this::isAutoMapperAnnotation);
if (!hasAutoMapper) {
return false;
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "start refresh properties");
refreshProperties(annotations, roundEnv);
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "start write config class");
writeConfigClass();
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "start generate mapper class");
annotations.stream()
.filter(this::isAutoMapperAnnotation)
.forEach(annotation -> processAutoMapperAnnotation(roundEnv, annotation));
return false;
}
private void refreshProperties(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
annotations.stream()
.filter(this::isMapperConfigAnnotation)
.findFirst()
.flatMap(annotation -> roundEnv.getElementsAnnotatedWith(annotation).stream().findFirst())
.ifPresent(element -> {
final MapperConfig mapperConfig = element.getAnnotation(MapperConfig.class);
String mapperPackage = StringUtils.isEmpty(mapperConfig.mapperPackage()) ? getPackageName(element)
: mapperConfig.mapperPackage();
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "mapper package " + mapperPackage);
AutoMapperProperties.setMapperPackage(mapperPackage);
});
}
private String getPackageName(Element element) {
return String.valueOf(processingEnv.getElementUtils().getPackageOf(element).getQualifiedName());
}
private void writeConfigClass() {
mapperConfigGenerator.write(processingEnv);
}
private void processAutoMapperAnnotation(final RoundEnvironment roundEnv, final TypeElement annotation) {
final List<AutoMapperMetadata> autoMapperMetadataList = roundEnv.getElementsAnnotatedWith(annotation)
.stream()
.map(this::buildAutoMapperMetadata)
.filter(Objects::nonNull)
.collect(Collectors.toList());
autoMapperMetadataList.forEach(autoMapperMetadata -> mapperSet.add(autoMapperMetadata.mapperName()));
List<AutoMapperMetadata> reverseMapperMetadataList = new ArrayList<>();
autoMapperMetadataList.forEach(autoMapperMetadata -> {
final AutoMapperMetadata reverseMapperMetadata = reverseMapper(autoMapperMetadata);
if (!mapperSet.add(reverseMapperMetadata.mapperName())) {
return;
}
reverseMapperMetadataList.add(reverseMapperMetadata);
});
autoMapperMetadataList.addAll(reverseMapperMetadataList);
autoMapperMetadataList.forEach(this::writeAutoMapperClassFile);
adapterMapperGenerator.write(processingEnv, methodMap.values());
}
private AutoMapperMetadata reverseMapper(AutoMapperMetadata autoMapperMetadata) {
AutoMapperMetadata reverseMapperMetadata = new AutoMapperMetadata();
reverseMapperMetadata.setSourceClassName(autoMapperMetadata.getTargetClassName());
reverseMapperMetadata.setTargetClassName(autoMapperMetadata.getSourceClassName());
// 需要继承的属性
final List<AutoMappingMetadata> fieldMetadataList = autoMapperMetadata.getFieldMappingList().stream()
.map(fieldMapping -> {
final AutoMappingMetadata autoMappingMetadata = new AutoMappingMetadata();
autoMappingMetadata.setSource(fieldMapping.getTarget());
autoMappingMetadata.setTarget(fieldMapping.getSource());
return autoMappingMetadata;
}).collect(Collectors.toList());
reverseMapperMetadata.setFieldMappingList(fieldMetadataList);
return reverseMapperMetadata;
}
private void writeAutoMapperClassFile(AutoMapperMetadata metadata) {
String mapperPackage = AutoMapperProperties.getMapperPackage();
String mapperClassName = metadata.mapperName();
try (final Writer writer = processingEnv.getFiler()
.createSourceFile(mapperPackage + "." + mapperClassName)
.openWriter()) {
mapperGenerator.write(metadata, writer);
addAdapterMethod(metadata.getSourceClassName(), metadata.getTargetClassName(),
ClassName.get(mapperPackage, mapperClassName));
} catch (IOException e) {
processingEnv.getMessager()
.printMessage(ERROR,
"Error while opening " + metadata.mapperName() + " output file: " + e.getMessage());
}
}
private void addAdapterMethod(ClassName source, ClassName target, ClassName mapper) {
AdapterMethodMetadata adapterMethodMetadata = AdapterMethodMetadata.newInstance(source, target, mapper);
methodMap.put(adapterMethodMetadata.getMethodName(), adapterMethodMetadata);
}
private AutoMapperMetadata buildAutoMapperMetadata(final Element ele) {
AutoMapper autoMapperAnnotation = ele.getAnnotation(AutoMapper.class);
if (autoMapperAnnotation == null) {
return null;
}
ClassName source = ClassName.get((TypeElement) ele);
ClassName target = transToClassName(autoMapperAnnotation::target);
if (target == null) {
return null;
}
List<ClassName> uses = transToClassNameList(autoMapperAnnotation::uses);
List<AutoMappingMetadata> autoMappingMetadataList = buildFieldMappingMetadata((TypeElement) ele);
AutoMapperMetadata metadata = new AutoMapperMetadata();
metadata.setSourceClassName(source);
metadata.setTargetClassName(target);
metadata.setUsesClassNameList(uses);
metadata.setFieldMappingList(autoMappingMetadataList);
return metadata;
}
private List<AutoMappingMetadata> buildFieldMappingMetadata(final TypeElement autoMapperEle) {
List<AutoMappingMetadata> list = new ArrayList<>();
if (!autoMapperEle.getKind().isClass()) {
return list;
}
for (Element ele : autoMapperEle.getEnclosedElements()) {
if (ele.getKind() != ElementKind.FIELD) {
continue;
}
AutoMapping autoMapping = ele.getAnnotation(AutoMapping.class);
if (autoMapping == null) {
continue;
}
AutoMappingMetadata metadata = new AutoMappingMetadata();
metadata.setTarget(autoMapping.target());
metadata.setSource(ele.getSimpleName().toString());
metadata.setIgnore(autoMapping.ignore());
list.add(metadata);
}
return list;
}
private ClassName transToClassName(Supplier<Class<?>> classSupplier) {
TypeMirror typeMirror = null;
try {
Class<?> targetClass = classSupplier.get();
} catch (MirroredTypeException e) {
typeMirror = e.getTypeMirror();
}
if (typeMirror == null) {
return null;
}
return (ClassName) ClassName.get(typeMirror);
}
private List<ClassName> transToClassNameList(Supplier<Class<?>[]> classSuppler) {
List<? extends TypeMirror> typeMirrors = null;
try {
Class<?>[] classes = classSuppler.get();
} catch (MirroredTypesException e) {
typeMirrors = e.getTypeMirrors();
}
return typeMirrors.stream()
.map(typeMirror -> (ClassName) ClassName.get(typeMirror))
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,35 @@
package io.github.linpl.processor;
import static io.github.linpl.processor.Constants.*;
public class AutoMapperProperties {
private static String mapperPackage = DEFAULT_MAPPER_PACKAGE;
private static String adapterPackage = mapperPackage;
private static String adapterClassName = DEFAULT_ADAPTER_CLASS_NAME;
private static String configClassName = AUTO_MAPPER_CONFIG_CLASS_NAME;
public static String getMapperPackage() {
return mapperPackage;
}
public static String getAdapterPackage() {
return getMapperPackage();
}
public static String getAdapterClassName() {
return adapterClassName;
}
public static String getConfigClassName() {
return configClassName;
}
public static void setMapperPackage(final String mapperPackage) {
AutoMapperProperties.mapperPackage = mapperPackage;
}
}

View File

@ -0,0 +1,64 @@
package io.github.linpl.processor;
public class AutoMappingMetadata {
private String target = "";
private String source = "";
private String dateFormat = "";
private String numberFormat = "";
private String expression = "";
private boolean ignore = false;
public String getTarget() {
return target;
}
public void setTarget(final String target) {
this.target = target;
}
public String getSource() {
return source;
}
public void setSource(final String source) {
this.source = source;
}
public String getDateFormat() {
return dateFormat;
}
public void setDateFormat(final String dateFormat) {
this.dateFormat = dateFormat;
}
public String getNumberFormat() {
return numberFormat;
}
public void setNumberFormat(final String numberFormat) {
this.numberFormat = numberFormat;
}
public String getExpression() {
return expression;
}
public void setExpression(final String expression) {
this.expression = expression;
}
public boolean isIgnore() {
return ignore;
}
public void setIgnore(final boolean ignore) {
this.ignore = ignore;
}
}

View File

@ -0,0 +1,27 @@
package io.github.linpl.processor;
public class Constants {
private static final String DEFAULT_BASE_PACKAGE = AutoMapperProcessor.class.getPackage().getName();
public static final String DEFAULT_MAPPER_PACKAGE = DEFAULT_BASE_PACKAGE + ".mappers";
public static final String DEFAULT_ADAPTER_CLASS_NAME = "ConvertMapperAdapter";
public static final String AUTO_MAPPER_CONFIG_CLASS_NAME = "AutoMapperConfig";
public static final String AUTO_MAPPER_ANNOTATION = "io.github.linpl.annotations.AutoMapper";
public static final String MAPPER_CONFIG_ANNOTATION = "io.github.linpl.annotations.MapperConfig";
public static final String BASE_MAPPER_PACKAGE = "io.github.linpl";
public static final String BASE_MAPPER_CLASS_NAME = "BaseMapper";
public static final String MAPSTRUCT_MAPPER_PACKAGE = "org.mapstruct";
public static final String MAPSTRUCT_MAPPER_CLASS_NAME = "Mapper";
}

View File

@ -0,0 +1,50 @@
package io.github.linpl.processor;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.TypeSpec;
import io.github.linpl.annotations.AutoMapper;
import java.io.IOException;
import java.io.Writer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Modifier;
import static javax.tools.Diagnostic.Kind.ERROR;
public class MapperConfigGenerator {
public void write(ProcessingEnvironment processingEnv) {
try (final Writer writer = processingEnv.getFiler()
.createSourceFile(AutoMapperProperties.getMapperPackage() + "." + AutoMapperProperties.getConfigClassName())
.openWriter()) {
JavaFile.builder(AutoMapperProperties.getMapperPackage(), createConfigTypeSpec()).build().writeTo(writer);
} catch (IOException e) {
processingEnv.getMessager()
.printMessage(ERROR,
"Error while opening " + AutoMapperProperties.getConfigClassName() + " output file: " +
e.getMessage());
}
}
private TypeSpec createConfigTypeSpec() {
return TypeSpec.interfaceBuilder(AutoMapperProperties.getConfigClassName())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(buildMapperConfigAnnotationSpec())
.build();
}
private AnnotationSpec buildMapperConfigAnnotationSpec() {
CodeBlock.Builder usesCodeBuilder = CodeBlock.builder().add("{");
usesCodeBuilder.add("$T.class",
ClassName.get(AutoMapperProperties.getAdapterPackage(), AutoMapperProperties.getAdapterClassName()));
CodeBlock usesCodeBlock = usesCodeBuilder.add("}").build();
return AnnotationSpec.builder(ClassName.get("org.mapstruct", "MapperConfig"))
.addMember("componentModel", "\"spring\"")
.addMember("uses", usesCodeBlock)
.build();
}
}

View File

@ -0,0 +1 @@
io.github.linpl.processor.AutoMapperProcessor

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mapstruct-plus</artifactId>
<groupId>io.github.linpl</groupId>
<version>${mapstruct-plus.version}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>2.4.5</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.github.linpl</groupId>
<artifactId>mapstruct-plus-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,22 @@
package io.github.linpl.mapstruct;
import io.github.linpl.Converter;
import io.github.linpl.ConverterFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MapstructAutoConfiguration {
@Bean
public ConverterFactory converterFactory(ApplicationContext applicationContext) {
return new SpringConverterFactory(applicationContext);
}
@Bean
public Converter converter(ConverterFactory converterFactory) {
return new Converter(converterFactory);
}
}

View File

@ -0,0 +1,26 @@
package io.github.linpl.mapstruct;
import io.github.linpl.BaseMapper;
import io.github.linpl.ConverterFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
public class SpringConverterFactory implements ConverterFactory {
private final ApplicationContext applicationContext;
public SpringConverterFactory(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public <S, T> BaseMapper<S, T> getMapper(final Class<S> sourceType, final Class<T> targetType) {
ResolvableType type = ResolvableType.forClassWithGenerics(
BaseMapper.class, sourceType, targetType);
String[] beanNames = applicationContext.getBeanNamesForType(type);
if (beanNames.length == 0) {
return null;
}
return (BaseMapper<S, T>) applicationContext.getBean(beanNames[0]);
}
}

View File

@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=io.github.linpl.mapstruct.MapstructAutoConfiguration

98
pom.xml Normal file
View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.linpl</groupId>
<artifactId>mapstruct-plus</artifactId>
<packaging>pom</packaging>
<version>${mapstruct-plus.version}</version>
<modules>
<module>mapstruct-plus-core</module>
<module>mapstruct-plus-spring-boot-starter</module>
<module>mapstruct-plus-processor</module>
</modules>
<properties>
<mapstruct-plus.version>1.0.0</mapstruct-plus.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.github.linpl</groupId>
<artifactId>mapstruct-plus-core</artifactId>
<version>${mapstruct-plus.version}</version>
</dependency>
<dependency>
<groupId>io.github.linpl</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
<version>${mapstruct-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baidu.lbsyun</groupId>
<artifactId>javapoet</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.1.Final</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.23</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.1.0</version>
<configuration>
<updatePomFile>true</updatePomFile>
<flattenMode>resolveCiFriendliesOnly</flattenMode>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.1.0</version>
</plugin>
</plugins>
</build>
</project>